From 9075b6e1cd246d2f4eec79e98ecadc4edf08fdfe Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Mon, 3 Aug 2020 11:15:49 +0100 Subject: [PATCH 01/74] [Security Solution] Disable bulk actions for immutable timeline templates (#73687) * disablebulk actions for immutable timeline templates * make immutable timelines not selectable * hide selected count if timeline status is immutable Co-authored-by: Elastic Machine --- .../components/open_timeline/index.tsx | 2 + .../open_timeline/open_timeline.test.tsx | 135 +++++++++++++++++- .../open_timeline/open_timeline.tsx | 53 ++++--- .../open_timeline_modal_body.test.tsx | 3 +- .../timelines_table/actions_columns.test.tsx | 29 ++++ .../open_timeline/timelines_table/index.tsx | 5 +- .../components/open_timeline/types.ts | 3 + 7 files changed, 204 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index 188b8979f613c2..4c5db80a6c916a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -326,6 +326,7 @@ export const StatefulOpenTimelineComponent = React.memo( sortField={sortField} templateTimelineFilter={templateTimelineFilter} timelineType={timelineType} + timelineStatus={timelineStatus} timelineFilter={timelineTabs} title={title} totalSearchResultsCount={totalCount} @@ -356,6 +357,7 @@ export const StatefulOpenTimelineComponent = React.memo( sortField={sortField} templateTimelineFilter={templateTimelineFilter} timelineType={timelineType} + timelineStatus={timelineStatus} timelineFilter={timelineFilters} title={title} totalSearchResultsCount={totalCount} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx index 57a6431a06b90a..9de3242c5e3038 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx @@ -17,7 +17,7 @@ import { TimelinesTableProps } from './timelines_table'; import { mockTimelineResults } from '../../../common/mock/timeline_results'; import { OpenTimeline } from './open_timeline'; import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from './constants'; -import { TimelineType } from '../../../../common/types/timeline'; +import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; jest.mock('../../../common/lib/kibana'); @@ -50,6 +50,7 @@ describe('OpenTimeline', () => { sortField: DEFAULT_SORT_FIELD, title, timelineType: TimelineType.default, + timelineStatus: TimelineStatus.active, templateTimelineFilter: [
], totalSearchResultsCount: mockSearchResults.length, }); @@ -263,4 +264,136 @@ describe('OpenTimeline', () => { `Showing: ${mockResults.length} timelines with "How was your day?"` ); }); + + test("it should render bulk actions if timelineStatus is active (selecting custom templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.active, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(true); + }); + + test("it should render a selectable timeline table if timelineStatus is active (selecting custom templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.active, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow') + ).toEqual(['createFrom', 'duplicate', 'export', 'selectable', 'delete']); + }); + + test("it should render selected count if timelineStatus is active (selecting custom templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.active, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(true); + }); + + test("it should not render bulk actions if timelineStatus is immutable (selecting Elastic templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.immutable, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(false); + }); + + test("it should not render a selectable timeline table if timelineStatus is immutable (selecting Elastic templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.immutable, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow') + ).toEqual(['createFrom', 'duplicate']); + }); + + test("it should not render selected count if timelineStatus is immutable (selecting Elastic templates' tab)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: TimelineStatus.immutable, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(false); + }); + + test("it should render bulk actions if timelineStatus is null (no template timelines' tab selected)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(true); + }); + + test("it should render a selectable timeline table if timelineStatus is null (no template timelines' tab selected)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow') + ).toEqual(['createFrom', 'duplicate', 'export', 'selectable', 'delete']); + }); + + test("it should render selected count if timelineStatus is null (no template timelines' tab selected)", () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(true); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index d839a1deddf216..c9495c46d4acf3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -8,7 +8,7 @@ import { EuiPanel, EuiBasicTable } from '@elastic/eui'; import React, { useCallback, useMemo, useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TimelineType } from '../../../../common/types/timeline'; +import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; import { ImportDataModal } from '../../../common/components/import_data_modal'; import { UtilityBarGroup, @@ -55,6 +55,7 @@ export const OpenTimeline = React.memo( setImportDataModalToggle, sortField, timelineType = TimelineType.default, + timelineStatus, timelineFilter, templateTimelineFilter, totalSearchResultsCount, @@ -140,19 +141,23 @@ export const OpenTimeline = React.memo( }, [setImportDataModalToggle, refetch, searchResults, totalSearchResultsCount]); const actionTimelineToShow = useMemo(() => { - const timelineActions: ActionTimelineToShow[] = [ - 'createFrom', - 'duplicate', - 'export', - 'selectable', - ]; + const timelineActions: ActionTimelineToShow[] = ['createFrom', 'duplicate']; - if (onDeleteSelected != null && deleteTimelines != null) { + if (timelineStatus !== TimelineStatus.immutable) { + timelineActions.push('export'); + timelineActions.push('selectable'); + } + + if ( + onDeleteSelected != null && + deleteTimelines != null && + timelineStatus !== TimelineStatus.immutable + ) { timelineActions.push('delete'); } return timelineActions; - }, [onDeleteSelected, deleteTimelines]); + }, [onDeleteSelected, deleteTimelines, timelineStatus]); const SearchRowContent = useMemo(() => <>{templateTimelineFilter}, [templateTimelineFilter]); @@ -206,20 +211,24 @@ export const OpenTimeline = React.memo( - - - {timelineType === TimelineType.template - ? i18n.SELECTED_TEMPLATES(selectedItems.length) - : i18n.SELECTED_TIMELINES(selectedItems.length)} - - - {i18n.BATCH_ACTIONS} - + {timelineStatus !== TimelineStatus.immutable && ( + <> + + {timelineType === TimelineType.template + ? i18n.SELECTED_TEMPLATES(selectedItems.length) + : i18n.SELECTED_TIMELINES(selectedItems.length)} + + + {i18n.BATCH_ACTIONS} + + + )} {i18n.REFRESH} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx index 12df17ceba6669..9632b0e6ecea4b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx @@ -17,7 +17,7 @@ import { TimelinesTableProps } from '../timelines_table'; import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineModalBody } from './open_timeline_modal_body'; import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants'; -import { TimelineType } from '../../../../../common/types/timeline'; +import { TimelineType, TimelineStatus } from '../../../../../common/types/timeline'; jest.mock('../../../../common/lib/kibana'); @@ -48,6 +48,7 @@ describe('OpenTimelineModal', () => { sortDirection: DEFAULT_SORT_DIRECTION, sortField: DEFAULT_SORT_FIELD, timelineType: TimelineType.default, + timelineStatus: TimelineStatus.active, templateTimelineFilter: [
], title, totalSearchResultsCount: mockSearchResults.length, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx index eddfdf6e01df26..52b7a4293e847d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx @@ -12,6 +12,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import '../../../../common/mock/match_media'; + import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineResult } from '../types'; import { TimelinesTableProps } from '.'; @@ -233,4 +234,32 @@ describe('#getActionsColumns', () => { expect(enableExportTimelineDownloader).toBeCalledWith(mockResults[0]); }); + + test('it should not render "export timeline" if it is not included', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + actionTimelineToShow: ['createFrom', 'duplicate'], + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="export-timeline"]').exists()).toEqual(false); + }); + + test('it should not render "delete timeline" if it is not included', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + actionTimelineToShow: ['createFrom', 'duplicate'], + }; + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="delete-timeline"]').exists()).toEqual(false); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx index 2d3672b15dd10c..d2fba696d9d54d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx @@ -24,7 +24,7 @@ import { getActionsColumns } from './actions_columns'; import { getCommonColumns } from './common_columns'; import { getExtendedColumns } from './extended_columns'; import { getIconHeaderColumns } from './icon_header_columns'; -import { TimelineTypeLiteralWithNull } from '../../../../../common/types/timeline'; +import { TimelineTypeLiteralWithNull, TimelineStatus } from '../../../../../common/types/timeline'; // there are a number of type mismatches across this file const EuiBasicTable: any = _EuiBasicTable; // eslint-disable-line @typescript-eslint/no-explicit-any @@ -159,7 +159,8 @@ export const TimelinesTable = React.memo( }; const selection = { - selectable: (timelineResult: OpenTimelineResult) => timelineResult.savedObjectId != null, + selectable: (timelineResult: OpenTimelineResult) => + timelineResult.savedObjectId != null && timelineResult.status !== TimelineStatus.immutable, selectableMessage: (selectable: boolean) => !selectable ? i18n.MISSING_SAVED_OBJECT_ID : undefined, onSelectionChange, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index eb5a03baad88c2..8950f814d6965d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -14,6 +14,7 @@ import { TimelineStatus, TemplateTimelineTypeLiteral, RowRendererId, + TimelineStatusLiteralWithNull, } from '../../../../common/types/timeline'; /** The users who added a timeline to favorites */ @@ -174,6 +175,8 @@ export interface OpenTimelineProps { sortField: string; /** this affects timeline's behaviour like editable / duplicatible */ timelineType: TimelineTypeLiteralWithNull; + /* active or immutable */ + timelineStatus: TimelineStatusLiteralWithNull; /** when timelineType === template, templatetimelineFilter is a JSX.Element */ templateTimelineFilter: JSX.Element[] | null; /** timeline / timeline template */ From 1b2233a5a69ff0fae714da1cc60934e0acf0df6d Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 3 Aug 2020 12:16:39 +0200 Subject: [PATCH 02/74] [ Functional Test ] Unskip X-Pack "Advanced Settings - spaces feature controls" suite (#73823) Co-authored-by: Elastic Machine --- .../feature_controls/advanced_settings_spaces.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts index c8adb3ce67d555..f1da94febc631f 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -14,8 +14,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const config = getService('config'); - // FLAKY: https://github.com/elastic/kibana/issues/57413 - describe.skip('spaces feature controls', () => { + describe('spaces feature controls', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); }); From d19883f624d8cbe654a6ba91e12edcf4722c88e3 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 3 Aug 2020 11:44:40 +0100 Subject: [PATCH 03/74] [ML] Adding combined job and datafeed JSON editing (#72117) * [ML] Fixing edit datafeed usablility issues * updates * add json editing to all wizard steps * removing unused include * adding comments * updating text * text update * wrapping preview in useCallback Co-authored-by: Elastic Machine --- .../datafeed_preview.tsx | 119 ++++++++++++++++++ .../datafeed_preview_flyout.tsx | 88 ++----------- .../common/datafeed_preview_flyout/index.ts | 1 + .../json_editor_flyout/json_editor_flyout.tsx | 112 ++++++++++++++--- .../components/datafeed_step/datafeed.tsx | 18 +-- .../job_details_step/job_details.tsx | 14 ++- .../pick_fields_step/pick_fields.tsx | 42 +++---- .../pages/components/summary_step/summary.tsx | 15 +-- 8 files changed, 262 insertions(+), 147 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx new file mode 100644 index 00000000000000..0dd802855ea67e --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { FC, useState, useEffect, useMemo, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiSpacer, + EuiLoadingSpinner, + EuiButton, +} from '@elastic/eui'; + +import { CombinedJob } from '../../../../../../../../common/types/anomaly_detection_jobs'; +import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; +import { mlJobService } from '../../../../../../services/job_service'; +import { ML_DATA_PREVIEW_COUNT } from '../../../../../../../../common/util/job_utils'; + +export const DatafeedPreview: FC<{ + combinedJob: CombinedJob | null; + heightOffset?: number; +}> = ({ combinedJob, heightOffset = 0 }) => { + // the ace editor requires a fixed height + const editorHeight = useMemo(() => `${window.innerHeight - 230 - heightOffset}px`, [ + heightOffset, + ]); + const [loading, setLoading] = useState(false); + const [previewJsonString, setPreviewJsonString] = useState(''); + const [outOfDate, setOutOfDate] = useState(false); + const [combinedJobString, setCombinedJobString] = useState(''); + + useEffect(() => { + try { + if (combinedJob !== null) { + if (combinedJobString === '') { + // first time, set the string and load the preview + loadDataPreview(); + } else { + setOutOfDate(JSON.stringify(combinedJob) !== combinedJobString); + } + } + } catch (error) { + // fail silently + } + }, [combinedJob]); + + const loadDataPreview = useCallback(async () => { + setPreviewJsonString(''); + if (combinedJob === null) { + return; + } + + setLoading(true); + setCombinedJobString(JSON.stringify(combinedJob)); + + if (combinedJob.datafeed_config && combinedJob.datafeed_config.indices.length) { + try { + const resp = await mlJobService.searchPreview(combinedJob); + const data = resp.aggregations + ? resp.aggregations.buckets.buckets.slice(0, ML_DATA_PREVIEW_COUNT) + : resp.hits.hits; + + setPreviewJsonString(JSON.stringify(data, null, 2)); + } catch (error) { + setPreviewJsonString(JSON.stringify(error, null, 2)); + } + setLoading(false); + setOutOfDate(false); + } else { + const errorText = i18n.translate( + 'xpack.ml.newJob.wizard.datafeedPreviewFlyout.datafeedDoesNotExistLabel', + { + defaultMessage: 'Datafeed does not exist', + } + ); + setPreviewJsonString(errorText); + } + }, [combinedJob]); + + return ( + + + + +
+ +
+
+
+ + {outOfDate && ( + + Refresh + + )} + +
+ + {loading === true ? ( + + + + + + + ) : ( + + )} +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx index 03be38adfbbe0c..d35083ec6e4792 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useState, useContext, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; +import React, { Fragment, FC, useState, useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlyout, @@ -13,18 +12,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, - EuiTitle, EuiFlyoutBody, - EuiSpacer, - EuiLoadingSpinner, } from '@elastic/eui'; -import { CombinedJob } from '../../../../../../../../common/types/anomaly_detection_jobs'; -import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; + import { JobCreatorContext } from '../../job_creator_context'; -import { mlJobService } from '../../../../../../services/job_service'; -import { ML_DATA_PREVIEW_COUNT } from '../../../../../../../../common/util/job_utils'; +import { DatafeedPreview } from './datafeed_preview'; -const EDITOR_HEIGHT = '800px'; export enum EDITOR_MODE { HIDDEN, READONLY, @@ -36,50 +29,11 @@ interface Props { export const DatafeedPreviewFlyout: FC = ({ isDisabled }) => { const { jobCreator } = useContext(JobCreatorContext); const [showFlyout, setShowFlyout] = useState(false); - const [previewJsonString, setPreviewJsonString] = useState(''); - const [loading, setLoading] = useState(false); function toggleFlyout() { setShowFlyout(!showFlyout); } - useEffect(() => { - if (showFlyout === true) { - loadDataPreview(); - } - }, [showFlyout]); - - async function loadDataPreview() { - setLoading(true); - setPreviewJsonString(''); - const combinedJob: CombinedJob = { - ...jobCreator.jobConfig, - datafeed_config: jobCreator.datafeedConfig, - }; - - if (combinedJob.datafeed_config && combinedJob.datafeed_config.indices.length) { - try { - const resp = await mlJobService.searchPreview(combinedJob); - const data = resp.aggregations - ? resp.aggregations.buckets.buckets.slice(0, ML_DATA_PREVIEW_COUNT) - : resp.hits.hits; - - setPreviewJsonString(JSON.stringify(data, null, 2)); - } catch (error) { - setPreviewJsonString(JSON.stringify(error, null, 2)); - } - setLoading(false); - } else { - const errorText = i18n.translate( - 'xpack.ml.newJob.wizard.datafeedPreviewFlyout.datafeedDoesNotExistLabel', - { - defaultMessage: 'Datafeed does not exist', - } - ); - setPreviewJsonString(errorText); - } - } - return ( @@ -87,12 +41,11 @@ export const DatafeedPreviewFlyout: FC = ({ isDisabled }) => { {showFlyout === true && isDisabled === false && ( setShowFlyout(false)} hideCloseButton size="m"> - @@ -127,28 +80,3 @@ const FlyoutButton: FC<{ isDisabled: boolean; onClick(): void }> = ({ isDisabled ); }; - -const Contents: FC<{ - title: string; - value: string; - loading: boolean; -}> = ({ title, value, loading }) => { - return ( - - -
{title}
-
- - {loading === true ? ( - - - - - - - ) : ( - - )} -
- ); -}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts index d52ed1364452a9..e96f374213eb30 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts @@ -4,3 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ export { DatafeedPreviewFlyout } from './datafeed_preview_flyout'; +export { DatafeedPreview } from './datafeed_preview'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx index dd5c8aa3e280a1..29d55e6ae48e02 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useState, useContext, useEffect } from 'react'; +import React, { Fragment, FC, useState, useContext, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -17,19 +17,21 @@ import { EuiTitle, EuiFlyoutBody, EuiSpacer, + EuiCallOut, } from '@elastic/eui'; import { collapseLiteralStrings } from '../../../../../../../../shared_imports'; -import { Datafeed } from '../../../../../../../../common/types/anomaly_detection_jobs'; +import { CombinedJob, Datafeed } from '../../../../../../../../common/types/anomaly_detection_jobs'; import { ML_EDITOR_MODE, MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; import { isValidJson } from '../../../../../../../../common/util/validation_utils'; import { JobCreatorContext } from '../../job_creator_context'; +import { DatafeedPreview } from '../datafeed_preview_flyout'; -const EDITOR_HEIGHT = '800px'; export enum EDITOR_MODE { HIDDEN, READONLY, EDITABLE, } +const WARNING_CALLOUT_OFFSET = 100; interface Props { isDisabled: boolean; jobEditorMode: EDITOR_MODE; @@ -38,21 +40,38 @@ interface Props { export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafeedEditorMode }) => { const { jobCreator, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); const [showJsonFlyout, setShowJsonFlyout] = useState(false); + const [showChangedIndicesWarning, setShowChangedIndicesWarning] = useState(false); const [jobConfigString, setJobConfigString] = useState(jobCreator.formattedJobJson); const [datafeedConfigString, setDatafeedConfigString] = useState( jobCreator.formattedDatafeedJson ); const [saveable, setSaveable] = useState(false); + const [tempCombinedJob, setTempCombinedJob] = useState(null); useEffect(() => { setJobConfigString(jobCreator.formattedJobJson); setDatafeedConfigString(jobCreator.formattedDatafeedJson); }, [jobCreatorUpdated]); + useEffect(() => { + if (showJsonFlyout === true) { + // when the flyout opens, update the JSON + setJobConfigString(jobCreator.formattedJobJson); + setDatafeedConfigString(jobCreator.formattedDatafeedJson); + setTempCombinedJob({ + ...JSON.parse(jobCreator.formattedJobJson), + datafeed_config: JSON.parse(jobCreator.formattedDatafeedJson), + }); + + setShowChangedIndicesWarning(false); + } else { + setTempCombinedJob(null); + } + }, [showJsonFlyout]); + const editJsonMode = - jobEditorMode === EDITOR_MODE.HIDDEN || datafeedEditorMode === EDITOR_MODE.HIDDEN; - const flyOutSize = editJsonMode ? 'm' : 'l'; + jobEditorMode === EDITOR_MODE.EDITABLE || datafeedEditorMode === EDITOR_MODE.EDITABLE; const readOnlyMode = jobEditorMode === EDITOR_MODE.READONLY && datafeedEditorMode === EDITOR_MODE.READONLY; @@ -64,6 +83,14 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee function onJobChange(json: string) { setJobConfigString(json); const valid = isValidJson(json); + setTempCombinedJob( + valid + ? { + ...JSON.parse(json), + datafeed_config: JSON.parse(datafeedConfigString), + } + : null + ); setSaveable(valid); } @@ -73,12 +100,22 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee let valid = isValidJson(jsonValue); if (valid) { // ensure that the user hasn't altered the indices list in the json. - const { indices }: Datafeed = JSON.parse(jsonValue); + const datafeed: Datafeed = JSON.parse(jsonValue); const originalIndices = jobCreator.indices.sort(); valid = - originalIndices.length === indices.length && - originalIndices.every((value, index) => value === indices[index]); + originalIndices.length === datafeed.indices.length && + originalIndices.every((value, index) => value === datafeed.indices[index]); + setShowChangedIndicesWarning(valid === false); + + setTempCombinedJob({ + ...JSON.parse(jobConfigString), + datafeed_config: datafeed, + }); + } else { + setShowChangedIndicesWarning(false); + setTempCombinedJob(null); } + setSaveable(valid); } @@ -99,7 +136,7 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee /> {showJsonFlyout === true && isDisabled === false && ( - setShowJsonFlyout(false)} hideCloseButton size={flyOutSize}> + setShowJsonFlyout(false)} hideCloseButton size={'l'}> {jobEditorMode !== EDITOR_MODE.HIDDEN && ( @@ -110,19 +147,51 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee defaultMessage: 'Job configuration JSON', })} value={jobConfigString} + heightOffset={showChangedIndicesWarning ? WARNING_CALLOUT_OFFSET : 0} /> )} {datafeedEditorMode !== EDITOR_MODE.HIDDEN && ( - + <> + + {datafeedEditorMode === EDITOR_MODE.EDITABLE && ( + + + + )} + )} + {showChangedIndicesWarning && ( + <> + + + + + + )} @@ -183,7 +252,12 @@ const Contents: FC<{ value: string; editJson: boolean; onChange(s: string): void; -}> = ({ title, value, editJson, onChange }) => { + heightOffset?: number; +}> = ({ title, value, editJson, onChange, heightOffset = 0 }) => { + // the ace editor requires a fixed height + const editorHeight = useMemo(() => `${window.innerHeight - 230 - heightOffset}px`, [ + heightOffset, + ]); return ( @@ -192,7 +266,7 @@ const Contents: FC<{ = ({ setCurrentStep, isCurrentStep }) => { const { jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); @@ -48,18 +47,11 @@ export const DatafeedStep: FC = ({ setCurrentStep, isCurrentStep }) = setCurrentStep(WIZARD_STEPS.PICK_FIELDS)} nextActive={nextActive}> - - - - - - - - +
)} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx index b0fb2e7267f7f1..bff99ad7c5281d 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx @@ -14,6 +14,8 @@ import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; import { AdvancedSection } from './components/advanced_section'; import { AdditionalSection } from './components/additional_section'; +import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; +import { isAdvancedJobCreator } from '../../../common/job_creator'; interface Props extends StepProps { advancedExpanded: boolean; @@ -30,7 +32,7 @@ export const JobDetailsStep: FC = ({ additionalExpanded, setAdditionalExpanded, }) => { - const { jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); + const { jobCreator, jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); const [nextActive, setNextActive] = useState(false); useEffect(() => { @@ -70,7 +72,15 @@ export const JobDetailsStep: FC = ({ previous={() => setCurrentStep(WIZARD_STEPS.PICK_FIELDS)} next={() => setCurrentStep(WIZARD_STEPS.VALIDATION)} nextActive={nextActive} - /> + > + {isAdvancedJobCreator(jobCreator) && ( + + )} + )} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx index 6f03b9a3c33211..2316383709164e 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx @@ -6,23 +6,26 @@ import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { JobCreatorContext } from '../job_creator_context'; import { WizardNav } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; -import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { SingleMetricView } from './components/single_metric_view'; import { MultiMetricView } from './components/multi_metric_view'; import { PopulationView } from './components/population_view'; import { AdvancedView } from './components/advanced_view'; import { CategorizationView } from './components/categorization_view'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; -import { DatafeedPreviewFlyout } from '../common/datafeed_preview_flyout'; +import { + isSingleMetricJobCreator, + isMultiMetricJobCreator, + isPopulationJobCreator, + isCategorizationJobCreator, + isAdvancedJobCreator, +} from '../../../common/job_creator'; export const PickFieldsStep: FC = ({ setCurrentStep, isCurrentStep }) => { const { jobCreator, jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); const [nextActive, setNextActive] = useState(false); - const jobType = jobCreator.type; useEffect(() => { setNextActive(jobValidator.isPickFieldsStepValid); @@ -32,25 +35,25 @@ export const PickFieldsStep: FC = ({ setCurrentStep, isCurrentStep }) {isCurrentStep && ( - {jobType === JOB_TYPE.SINGLE_METRIC && ( + {isSingleMetricJobCreator(jobCreator) && ( )} - {jobType === JOB_TYPE.MULTI_METRIC && ( + {isMultiMetricJobCreator(jobCreator) && ( )} - {jobType === JOB_TYPE.POPULATION && ( + {isPopulationJobCreator(jobCreator) && ( )} - {jobType === JOB_TYPE.ADVANCED && ( + {isAdvancedJobCreator(jobCreator) && ( )} - {jobType === JOB_TYPE.CATEGORIZATION && ( + {isCategorizationJobCreator(jobCreator) && ( )} setCurrentStep( - jobCreator.type === JOB_TYPE.ADVANCED + isAdvancedJobCreator(jobCreator) ? WIZARD_STEPS.ADVANCED_CONFIGURE_DATAFEED : WIZARD_STEPS.TIME_RANGE ) @@ -58,19 +61,12 @@ export const PickFieldsStep: FC = ({ setCurrentStep, isCurrentStep }) next={() => setCurrentStep(WIZARD_STEPS.JOB_DETAILS)} nextActive={nextActive} > - {jobType === JOB_TYPE.ADVANCED && ( - - - - - - - - + {isAdvancedJobCreator(jobCreator) && ( + )} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx index 5ef59951c43cce..24d7fb9fc2a40d 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx @@ -22,8 +22,6 @@ import { JobCreatorContext } from '../job_creator_context'; import { JobRunner } from '../../../common/job_runner'; import { mlJobService } from '../../../../../services/job_service'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; -import { DatafeedPreviewFlyout } from '../common/datafeed_preview_flyout'; -import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { getErrorMessage } from '../../../../../../../common/util/errors'; import { isSingleMetricJobCreator, isAdvancedJobCreator } from '../../../common/job_creator'; import { JobDetails } from './components/job_details'; @@ -54,13 +52,14 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => const [jobRunner, setJobRunner] = useState(null); const isAdvanced = isAdvancedJobCreator(jobCreator); + const jsonEditorMode = isAdvanced ? EDITOR_MODE.EDITABLE : EDITOR_MODE.READONLY; useEffect(() => { jobCreator.subscribeToProgress(setProgress); }, []); async function start() { - if (jobCreator.type === JOB_TYPE.ADVANCED) { + if (isAdvanced) { await startAdvanced(); } else { await startInline(); @@ -176,15 +175,11 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => 0} - jobEditorMode={EDITOR_MODE.READONLY} - datafeedEditorMode={EDITOR_MODE.READONLY} + jobEditorMode={jsonEditorMode} + datafeedEditorMode={jsonEditorMode} /> - {jobCreator.type === JOB_TYPE.ADVANCED ? ( - - - - ) : ( + {isAdvanced === false && ( Date: Mon, 3 Aug 2020 12:15:07 +0100 Subject: [PATCH 04/74] [ML] Add datafeed query reset button (#73958) * [ML] Add datafeed query reset button * changing id * adding translation * fix typo * default query refactor Co-authored-by: Elastic Machine --- .../components/reset_query/index.tsx | 7 ++ .../components/reset_query/reset_query.tsx | 75 +++++++++++++++++++ .../components/datafeed_step/datafeed.tsx | 2 + .../jobs/new_job/utils/new_job_utils.ts | 26 ++++--- 4 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/index.tsx create mode 100644 x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/reset_query.tsx diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/index.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/index.tsx new file mode 100644 index 00000000000000..151f600eafdbe2 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/index.tsx @@ -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 { ResetQueryButton } from './reset_query'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/reset_query.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/reset_query.tsx new file mode 100644 index 00000000000000..17558368f117db --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/reset_query.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { FC, useContext, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButtonEmpty, + EuiConfirmModal, + EuiOverlayMask, + EuiCodeBlock, + EuiSpacer, +} from '@elastic/eui'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { getDefaultDatafeedQuery } from '../../../../../utils/new_job_utils'; + +export const ResetQueryButton: FC = () => { + const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); + const [confirmModalVisible, setConfirmModalVisible] = useState(false); + const [defaultQueryString] = useState(JSON.stringify(getDefaultDatafeedQuery(), null, 2)); + + const closeModal = () => setConfirmModalVisible(false); + const showModal = () => setConfirmModalVisible(true); + + function resetDatafeed() { + jobCreator.query = getDefaultDatafeedQuery(); + jobCreatorUpdate(); + closeModal(); + } + return ( + <> + {confirmModalVisible && ( + + + + + + + + {defaultQueryString} + + + + )} + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx index 4223be2a2e3c4f..b9250c3ecdce53 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx @@ -11,6 +11,7 @@ import { QueryInput } from './components/query'; import { QueryDelayInput } from './components/query_delay'; import { FrequencyInput } from './components/frequency'; import { ScrollSizeInput } from './components/scroll_size'; +import { ResetQueryButton } from './components/reset_query'; import { TimeField } from './components/time_field'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; @@ -46,6 +47,7 @@ export const DatafeedStep: FC = ({ setCurrentStep, isCurrentStep }) = + setCurrentStep(WIZARD_STEPS.PICK_FIELDS)} nextActive={nextActive}> Date: Mon, 3 Aug 2020 14:49:17 +0200 Subject: [PATCH 05/74] [APM] Disable auto-refresh by default (#74068) --- .../DatePicker/__test__/DatePicker.test.tsx | 23 ++++++++----------- .../components/shared/DatePicker/index.tsx | 16 +------------ 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx index 36e33fba89fbbd..2434d898389d84 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx @@ -64,21 +64,20 @@ describe('DatePicker', () => { }); beforeEach(() => { - jest.clearAllMocks(); + jest.resetAllMocks(); }); - it('should set default query params in the URL', () => { + it('sets default query params in the URL', () => { mountDatePicker(); expect(mockHistoryPush).toHaveBeenCalledTimes(1); expect(mockHistoryPush).toHaveBeenCalledWith( expect.objectContaining({ - search: - 'rangeFrom=now-15m&rangeTo=now&refreshPaused=false&refreshInterval=10000', + search: 'rangeFrom=now-15m&rangeTo=now', }) ); }); - it('should add missing default value', () => { + it('adds missing default value', () => { mountDatePicker({ rangeTo: 'now', refreshInterval: 5000, @@ -86,13 +85,12 @@ describe('DatePicker', () => { expect(mockHistoryPush).toHaveBeenCalledTimes(1); expect(mockHistoryPush).toHaveBeenCalledWith( expect.objectContaining({ - search: - 'rangeFrom=now-15m&rangeTo=now&refreshInterval=5000&refreshPaused=false', + search: 'rangeFrom=now-15m&rangeTo=now&refreshInterval=5000', }) ); }); - it('should not set default query params in the URL when values already defined', () => { + it('does not set default query params in the URL when values already defined', () => { mountDatePicker({ rangeFrom: 'now-1d', rangeTo: 'now', @@ -102,7 +100,7 @@ describe('DatePicker', () => { expect(mockHistoryPush).toHaveBeenCalledTimes(0); }); - it('should update the URL when the date range changes', () => { + it('updates the URL when the date range changes', () => { const datePicker = mountDatePicker(); datePicker.find(EuiSuperDatePicker).props().onTimeChange({ start: 'updated-start', @@ -113,13 +111,12 @@ describe('DatePicker', () => { expect(mockHistoryPush).toHaveBeenCalledTimes(2); expect(mockHistoryPush).toHaveBeenLastCalledWith( expect.objectContaining({ - search: - 'rangeFrom=updated-start&rangeTo=updated-end&refreshInterval=5000&refreshPaused=false', + search: 'rangeFrom=updated-start&rangeTo=updated-end', }) ); }); - it('should auto-refresh when refreshPaused is false', async () => { + it('enables auto-refresh when refreshPaused is false', async () => { jest.useFakeTimers(); const wrapper = mountDatePicker({ refreshPaused: false, @@ -132,7 +129,7 @@ describe('DatePicker', () => { wrapper.unmount(); }); - it('should NOT auto-refresh when refreshPaused is true', async () => { + it('disables auto-refresh when refreshPaused is true', async () => { jest.useFakeTimers(); mountDatePicker({ refreshPaused: true, refreshInterval: 1000 }); expect(mockRefreshTimeRange).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx index 5201d80de5a122..403a8cad854cd2 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx @@ -14,11 +14,7 @@ import { useUrlParams } from '../../../hooks/useUrlParams'; import { clearCache } from '../../../services/rest/callApi'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common'; -import { - TimePickerQuickRange, - TimePickerTimeDefaults, - TimePickerRefreshInterval, -} from './typings'; +import { TimePickerQuickRange, TimePickerTimeDefaults } from './typings'; function removeUndefinedAndEmptyProps(obj: T): Partial { return pickBy(obj, (value) => value !== undefined && !isEmpty(String(value))); @@ -36,19 +32,9 @@ export function DatePicker() { UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS ); - const timePickerRefreshIntervalDefaults = core.uiSettings.get< - TimePickerRefreshInterval - >(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS); - const DEFAULT_VALUES = { rangeFrom: timePickerTimeDefaults.from, rangeTo: timePickerTimeDefaults.to, - refreshPaused: timePickerRefreshIntervalDefaults.pause, - /* - * Must be replaced by timePickerRefreshIntervalDefaults.value when this issue is fixed. - * https://github.com/elastic/kibana/issues/70562 - */ - refreshInterval: 10000, }; const commonlyUsedRanges = timePickerQuickRanges.map( From aada48e0deab04912956c842521635062eecf1fa Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Mon, 3 Aug 2020 15:39:26 +0100 Subject: [PATCH 06/74] [Security solution] Get all timeline (#72690) * get all timelines * update unit tests * fix cypress * rollback cypress rummer * add unit test Co-authored-by: Elastic Machine --- .../public/graphql/introspection.json | 36 ++- .../security_solution/public/graphql/types.ts | 11 +- .../server/graphql/timeline/resolvers.ts | 2 +- .../server/graphql/timeline/schema.gql.ts | 6 +- .../security_solution/server/graphql/types.ts | 13 +- .../timelines/find_timeline_by_filter.sh | 4 +- .../scripts/timelines/get_all_timelines.sh | 25 +- .../scripts/timelines/get_timeline_by_id.sh | 2 +- .../get_timeline_by_template_timeline_id.sh | 2 +- .../routes/__mocks__/import_timelines.ts | 18 ++ .../routes/__mocks__/request_responses.ts | 4 +- ...ute.test.ts => get_timeline_route.test.ts} | 28 ++- ...e_by_id_route.ts => get_timeline_route.ts} | 31 ++- .../schemas/get_timeline_by_id_schema.ts | 11 +- .../routes/utils/check_timelines_status.ts | 2 +- .../timeline/routes/utils/export_timelines.ts | 4 +- .../timeline/routes/utils/get_timelines.ts | 34 --- .../timeline/routes/utils/import_timelines.ts | 28 ++- .../server/lib/timeline/saved_object.test.ts | 230 +++++++++++++++++- .../server/lib/timeline/saved_object.ts | 40 +-- .../security_solution/server/routes/index.ts | 4 +- 21 files changed, 383 insertions(+), 152 deletions(-) rename x-pack/plugins/security_solution/server/lib/timeline/routes/{get_timeline_by_id_route.test.ts => get_timeline_route.test.ts} (66%) rename x-pack/plugins/security_solution/server/lib/timeline/routes/{get_timeline_by_id_route.ts => get_timeline_route.ts} (67%) delete mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 84096e242cbbdf..7b20873bf63ccb 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -229,7 +229,11 @@ { "name": "pageInfo", "description": "", - "type": { "kind": "INPUT_OBJECT", "name": "PageInfoTimeline", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "PageInfoTimeline", "ofType": null } + }, "defaultValue": null }, { @@ -10905,13 +10909,21 @@ { "name": "pageIndex", "description": "", - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, "defaultValue": null }, { "name": "pageSize", "description": "", - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, "defaultValue": null } ], @@ -13142,24 +13154,6 @@ "interfaces": null, "enumValues": null, "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "TemplateTimelineType", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "elastic", - "description": "", - "isDeprecated": false, - "deprecationReason": null - }, - { "name": "custom", "description": "", "isDeprecated": false, "deprecationReason": null } - ], - "possibleTypes": null } ], "directives": [ diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 90d1b8bd54df53..f7d2c81f536be1 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -102,9 +102,9 @@ export interface TlsSortField { } export interface PageInfoTimeline { - pageIndex?: Maybe; + pageIndex: number; - pageSize?: Maybe; + pageSize: number; } export interface SortTimeline { @@ -423,11 +423,6 @@ export enum FlowDirection { biDirectional = 'biDirectional', } -export enum TemplateTimelineType { - elastic = 'elastic', - custom = 'custom', -} - export type ToStringArrayNoNullable = any; export type ToIFieldSubTypeNonNullable = any; @@ -2324,7 +2319,7 @@ export interface GetOneTimelineQueryArgs { id: string; } export interface GetAllTimelineQueryArgs { - pageInfo?: Maybe; + pageInfo: PageInfoTimeline; search?: Maybe; diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts index f4a18a40f7d4be..9bd544f6942cf8 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts @@ -50,7 +50,7 @@ export const createTimelineResolvers = ( return libs.timeline.getAllTimeline( req, args.onlyUserFavorite || null, - args.pageInfo || null, + args.pageInfo, args.search || null, args.sort || null, args.status || null, diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index 58a13a7115b728..573539e1bb54f6 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -178,8 +178,8 @@ export const timelineSchema = gql` } input PageInfoTimeline { - pageIndex: Float - pageSize: Float + pageIndex: Float! + pageSize: Float! } enum SortFieldTimeline { @@ -316,7 +316,7 @@ export const timelineSchema = gql` extend type Query { getOneTimeline(id: ID!): TimelineResult! - getAllTimeline(pageInfo: PageInfoTimeline, search: String, sort: SortTimeline, onlyUserFavorite: Boolean, timelineType: TimelineType, status: TimelineStatus): ResponseTimelines! + getAllTimeline(pageInfo: PageInfoTimeline!, search: String, sort: SortTimeline, onlyUserFavorite: Boolean, timelineType: TimelineType, status: TimelineStatus): ResponseTimelines! } extend type Mutation { diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index ca0732816aa4d5..fa55af351651e7 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -104,9 +104,9 @@ export interface TlsSortField { } export interface PageInfoTimeline { - pageIndex?: Maybe; + pageIndex: number; - pageSize?: Maybe; + pageSize: number; } export interface SortTimeline { @@ -425,11 +425,6 @@ export enum FlowDirection { biDirectional = 'biDirectional', } -export enum TemplateTimelineType { - elastic = 'elastic', - custom = 'custom', -} - export type ToStringArrayNoNullable = any; export type ToIFieldSubTypeNonNullable = any; @@ -2326,7 +2321,7 @@ export interface GetOneTimelineQueryArgs { id: string; } export interface GetAllTimelineQueryArgs { - pageInfo?: Maybe; + pageInfo: PageInfoTimeline; search?: Maybe; @@ -2802,7 +2797,7 @@ export namespace QueryResolvers { TContext = SiemContext > = Resolver; export interface GetAllTimelineArgs { - pageInfo?: Maybe; + pageInfo: PageInfoTimeline; search?: Maybe; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh index 3dd8e7f1097f4b..f3b8a81f4086a9 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh @@ -14,13 +14,13 @@ STATUS=${1:-active} TIMELINE_TYPE=${2:-default} # Example get all timelines: -# ./timelines/find_timeline_by_filter.sh active +# sh ./timelines/find_timeline_by_filter.sh active # Example get all prepackaged timeline templates: # ./timelines/find_timeline_by_filter.sh immutable template # Example get all custom timeline templates: -# ./timelines/find_timeline_by_filter.sh active template +# sh ./timelines/find_timeline_by_filter.sh active template curl -s -k \ -H "Content-Type: application/json" \ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh index 335d1b8c86696c..05a9e0bd1ac97f 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh @@ -9,28 +9,11 @@ set -e ./check_env_variables.sh -# Example: ./timelines/get_all_timelines.sh +# Example: sh ./timelines/get_all_timelines.sh + curl -s -k \ -H "Content-Type: application/json" \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X POST "${KIBANA_URL}${SPACE_URL}/api/solutions/security/graphql" \ - -d '{ - "operationName": "GetAllTimeline", - "variables": { - "onlyUserFavorite": false, - "pageInfo": { - "pageIndex": null, - "pageSize": null - }, - "search": "", - "sort": { - "sortField": "updated", - "sortOrder": "desc" - }, - "status": "active", - "timelineType": null - }, - "query": "query GetAllTimeline($pageInfo: PageInfoTimeline!, $search: String, $sort: SortTimeline, $onlyUserFavorite: Boolean, $timelineType: TimelineType, $status: TimelineStatus) {\n getAllTimeline(pageInfo: $pageInfo, search: $search, sort: $sort, onlyUserFavorite: $onlyUserFavorite, timelineType: $timelineType, status: $status) {\n totalCount\n defaultTimelineCount\n templateTimelineCount\n elasticTemplateTimelineCount\n customTemplateTimelineCount\n favoriteCount\n timeline {\n savedObjectId\n description\n favorite {\n fullName\n userName\n favoriteDate\n __typename\n }\n eventIdToNoteIds {\n eventId\n note\n timelineId\n noteId\n created\n createdBy\n timelineVersion\n updated\n updatedBy\n version\n __typename\n }\n notes {\n eventId\n note\n timelineId\n timelineVersion\n noteId\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n noteIds\n pinnedEventIds\n status\n title\n timelineType\n templateTimelineId\n templateTimelineVersion\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n __typename\n }\n}\n" -}' | jq . - + -X GET "${KIBANA_URL}${SPACE_URL}/api/timeline" \ + | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh index 0c0694c0591f9c..13184ac6c6d567 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./timelines/get_timeline_by_id.sh {timeline_id} +# Example: sh ./timelines/get_timeline_by_id.sh {timeline_id} curl -s -k \ -H "Content-Type: application/json" \ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh index 36862b519130b5..87eddfbe6b9d45 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_timeline_by_template_timeline_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./timelines/get_timeline_by_template_timeline_id.sh {template_timeline_id} +# Example: sh ./timelines/get_timeline_by_template_timeline_id.sh {template_timeline_id} curl -s -k \ -H "Content-Type: application/json" \ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts index 0b10018de5bba5..245146dda183fe 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts @@ -1198,3 +1198,21 @@ export const mockCheckTimelinesStatusAfterInstallResult = { }, ], }; + +export const mockSavedObject = { + type: 'siem-ui-timeline', + id: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + attributes: { + savedQueryId: null, + + status: 'immutable', + + excludedRowRendererIds: [], + ...mockGetTemplateTimelineValue, + }, + references: [], + updated_at: '2020-07-21T12:03:08.901Z', + version: 'WzAsMV0=', + namespaces: ['default'], + score: 0.9444616, +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts index e3aeff280678ff..c5d69398b7f0c7 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -175,11 +175,11 @@ export const cleanDraftTimelinesRequest = (timelineType: TimelineType) => }, }); -export const getTimelineByIdRequest = (query: GetTimelineByIdSchemaQuery) => +export const getTimelineRequest = (query?: GetTimelineByIdSchemaQuery) => requestMock.create({ method: 'get', path: TIMELINE_URL, - query, + query: query ?? {}, }); export const installPrepackedTimelinesRequest = () => diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.test.ts similarity index 66% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.test.ts index 30528f8563ab8e..6f99739ae2e2b2 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.test.ts @@ -10,19 +10,24 @@ import { requestContextMock, createMockConfig, } from '../../detection_engine/routes/__mocks__'; +import { getAllTimeline } from '../saved_object'; import { mockGetCurrentUser } from './__mocks__/import_timelines'; -import { getTimelineByIdRequest } from './__mocks__/request_responses'; +import { getTimelineRequest } from './__mocks__/request_responses'; import { getTimeline, getTemplateTimeline } from './utils/create_timelines'; -import { getTimelineByIdRoute } from './get_timeline_by_id_route'; +import { getTimelineRoute } from './get_timeline_route'; jest.mock('./utils/create_timelines', () => ({ getTimeline: jest.fn(), getTemplateTimeline: jest.fn(), })); -describe('get timeline by id', () => { +jest.mock('../saved_object', () => ({ + getAllTimeline: jest.fn(), +})); + +describe('get timeline', () => { let server: ReturnType; let securitySetup: SecurityPluginSetup; let { context } = requestContextMock.createTools(); @@ -41,15 +46,12 @@ describe('get timeline by id', () => { authz: {}, } as unknown) as SecurityPluginSetup; - getTimelineByIdRoute(server.router, createMockConfig(), securitySetup); + getTimelineRoute(server.router, createMockConfig(), securitySetup); }); test('should call getTemplateTimeline if templateTimelineId is given', async () => { const templateTimelineId = '123'; - await server.inject( - getTimelineByIdRequest({ template_timeline_id: templateTimelineId }), - context - ); + await server.inject(getTimelineRequest({ template_timeline_id: templateTimelineId }), context); expect((getTemplateTimeline as jest.Mock).mock.calls[0][1]).toEqual(templateTimelineId); }); @@ -57,8 +59,16 @@ describe('get timeline by id', () => { test('should call getTimeline if id is given', async () => { const id = '456'; - await server.inject(getTimelineByIdRequest({ id }), context); + await server.inject(getTimelineRequest({ id }), context); expect((getTimeline as jest.Mock).mock.calls[0][1]).toEqual(id); }); + + test('should call getAllTimeline if nither templateTimelineId nor id is given', async () => { + (getAllTimeline as jest.Mock).mockResolvedValue({ totalCount: 3 }); + + await server.inject(getTimelineRequest(), context); + + expect(getAllTimeline as jest.Mock).toHaveBeenCalledTimes(2); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts similarity index 67% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts index c4957b9d4b9e26..f36adb648cc036 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_by_id_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts @@ -17,8 +17,10 @@ import { buildSiemResponse, transformError } from '../../detection_engine/routes import { buildFrameworkRequest } from './utils/common'; import { getTimelineByIdSchemaQuery } from './schemas/get_timeline_by_id_schema'; import { getTimeline, getTemplateTimeline } from './utils/create_timelines'; +import { getAllTimeline } from '../saved_object'; +import { TimelineStatus } from '../../../../common/types/timeline'; -export const getTimelineByIdRoute = ( +export const getTimelineRoute = ( router: IRouter, config: ConfigType, security: SetupPlugins['security'] @@ -34,12 +36,33 @@ export const getTimelineByIdRoute = ( async (context, request, response) => { try { const frameworkRequest = await buildFrameworkRequest(context, security, request); - const { template_timeline_id: templateTimelineId, id } = request.query; + const query = request.query ?? {}; + const { template_timeline_id: templateTimelineId, id } = query; let res = null; - if (templateTimelineId != null) { + if (templateTimelineId != null && id == null) { res = await getTemplateTimeline(frameworkRequest, templateTimelineId); - } else if (id != null) { + } else if (templateTimelineId == null && id != null) { res = await getTimeline(frameworkRequest, id); + } else if (templateTimelineId == null && id == null) { + const tempResult = await getAllTimeline( + frameworkRequest, + false, + { pageSize: 1, pageIndex: 1 }, + null, + null, + TimelineStatus.active, + null + ); + + res = await getAllTimeline( + frameworkRequest, + false, + { pageSize: tempResult?.totalCount ?? 0, pageIndex: 1 }, + null, + null, + TimelineStatus.active, + null + ); } return response.ok({ body: res ?? {} }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts index 2c6098bc75500f..65c956ed604400 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import * as rt from 'io-ts'; +import { unionWithNullType } from '../../../../../common/utility_types'; -export const getTimelineByIdSchemaQuery = rt.partial({ - template_timeline_id: rt.string, - id: rt.string, -}); +export const getTimelineByIdSchemaQuery = unionWithNullType( + rt.partial({ + template_timeline_id: rt.string, + id: rt.string, + }) +); export type GetTimelineByIdSchemaQuery = rt.TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts index 2ce2c37d4fa314..b5aa24336b2d7c 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts @@ -35,7 +35,7 @@ export const checkTimelinesStatus = async ( try { readStream = await getReadables(dataPath); - timeline = await getExistingPrepackagedTimelines(frameworkRequest, false); + timeline = await getExistingPrepackagedTimelines(frameworkRequest); } catch (err) { return { timelinesToInstall: [], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts index 6f194c3b8538ee..79ebf6280a19ea 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts @@ -20,7 +20,7 @@ import { FrameworkRequest } from '../../../framework'; import * as noteLib from '../../../note/saved_object'; import * as pinnedEventLib from '../../../pinned_event/saved_object'; -import { getTimelines } from '../../saved_object'; +import { getSelectedTimelines } from '../../saved_object'; const getGlobalEventNotesByTimelineId = (currentNotes: NoteSavedObject[]): ExportedNotes => { const initialNotes: ExportedNotes = { @@ -55,7 +55,7 @@ const getTimelinesFromObjects = async ( request: FrameworkRequest, ids?: string[] | null ): Promise> => { - const { timelines, errors } = await getTimelines(request, ids); + const { timelines, errors } = await getSelectedTimelines(request, ids); const exportedIds = timelines.map((t) => t.savedObjectId); const [notes, pinnedEvents] = await Promise.all([ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts deleted file mode 100644 index 1dac773ad6fde4..00000000000000 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FrameworkRequest } from '../../../framework'; -import { getTimelines as getSelectedTimelines } from '../../saved_object'; -import { TimelineSavedObject } from '../../../../../common/types/timeline'; - -export const getTimelines = async ( - frameworkRequest: FrameworkRequest, - ids: string[] -): Promise<{ timeline: TimelineSavedObject[] | null; error: string | null }> => { - try { - const timelines = await getSelectedTimelines(frameworkRequest, ids); - const existingTimelineIds = timelines.timelines.map((timeline) => timeline.savedObjectId); - const errorMsg = timelines.errors.reduce( - (acc, curr) => (acc ? `${acc}, ${curr.message}` : curr.message), - '' - ); - if (existingTimelineIds.length > 0) { - const message = existingTimelineIds.join(', '); - return { - timeline: timelines.timelines, - error: errorMsg ? `${message} found, ${errorMsg}` : null, - }; - } else { - return { timeline: null, error: errorMsg }; - } - } catch (e) { - return e.message; - } -}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts index 996dc5823691d6..1fea11f01bcc52 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts @@ -77,6 +77,24 @@ export const timelineSavedObjectOmittedFields = [ 'version', ]; +export const setTimeline = ( + parsedTimelineObject: Partial, + parsedTimeline: ImportedTimeline, + isTemplateTimeline: boolean +) => { + return { + ...parsedTimelineObject, + status: + parsedTimeline.status === TimelineStatus.draft + ? TimelineStatus.active + : parsedTimeline.status ?? TimelineStatus.active, + templateTimelineVersion: isTemplateTimeline + ? parsedTimeline.templateTimelineVersion ?? 1 + : null, + templateTimelineId: isTemplateTimeline ? parsedTimeline.templateTimelineId ?? uuid.v4() : null, + }; +}; + const CHUNK_PARSED_OBJECT_SIZE = 10; const DEFAULT_IMPORT_ERROR = `Something has gone wrong. We didn't handle something properly. To help us fix this, please upload your file to https://discuss.elastic.co/c/security/siem.`; @@ -151,15 +169,7 @@ export const importTimelines = async ( // create timeline / timeline template newTimeline = await createTimelines({ frameworkRequest, - timeline: { - ...parsedTimelineObject, - status: - status === TimelineStatus.draft - ? TimelineStatus.active - : status ?? TimelineStatus.active, - templateTimelineVersion: isTemplateTimeline ? templateTimelineVersion : null, - templateTimelineId: isTemplateTimeline ? templateTimelineId ?? uuid.v4() : null, - }, + timeline: setTimeline(parsedTimelineObject, parsedTimeline, isTemplateTimeline), pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds, notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes], isImmutable, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts index 3c4343b6428915..0ef83bb84c4c36 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts @@ -3,8 +3,30 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { FrameworkRequest } from '../framework'; +import { mockGetTimelineValue, mockSavedObject } from './routes/__mocks__/import_timelines'; -import { convertStringToBase64 } from './saved_object'; +import { + convertStringToBase64, + getExistingPrepackagedTimelines, + getAllTimeline, + AllTimelinesResponse, +} from './saved_object'; +import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline'; +import { getNotesByTimelineId } from '../note/saved_object'; +import { getAllPinnedEventsByTimelineId } from '../pinned_event/saved_object'; + +jest.mock('./convert_saved_object_to_savedtimeline', () => ({ + convertSavedObjectToSavedTimeline: jest.fn(), +})); + +jest.mock('../note/saved_object', () => ({ + getNotesByTimelineId: jest.fn().mockResolvedValue([]), +})); + +jest.mock('../pinned_event/saved_object', () => ({ + getAllPinnedEventsByTimelineId: jest.fn().mockResolvedValue([]), +})); describe('saved_object', () => { describe('convertStringToBase64', () => { @@ -22,4 +44,210 @@ describe('saved_object', () => { expect(convertStringToBase64('')).toBe(''); }); }); + + describe('getExistingPrepackagedTimelines', () => { + let mockFindSavedObject: jest.Mock; + let mockRequest: FrameworkRequest; + + beforeEach(() => { + mockFindSavedObject = jest.fn().mockResolvedValue({ saved_objects: [], total: 0 }); + mockRequest = ({ + user: { + username: 'username', + }, + context: { + core: { + savedObjects: { + client: { + find: mockFindSavedObject, + }, + }, + }, + }, + } as unknown) as FrameworkRequest; + }); + + afterEach(() => { + mockFindSavedObject.mockClear(); + (getNotesByTimelineId as jest.Mock).mockClear(); + (getAllPinnedEventsByTimelineId as jest.Mock).mockClear(); + }); + + test('should send correct options if countsOnly is true', async () => { + const contsOnly = true; + await getExistingPrepackagedTimelines(mockRequest, contsOnly); + expect(mockFindSavedObject).toBeCalledWith({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options if countsOnly is false', async () => { + const contsOnly = false; + await getExistingPrepackagedTimelines(mockRequest, contsOnly); + expect(mockFindSavedObject).toBeCalledWith({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and siem-ui-timeline.attributes.status: immutable', + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options if pageInfo is given', async () => { + const contsOnly = false; + const pageInfo = { + pageSize: 10, + pageIndex: 1, + }; + await getExistingPrepackagedTimelines(mockRequest, contsOnly, pageInfo); + expect(mockFindSavedObject).toBeCalledWith({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 10, + type: 'siem-ui-timeline', + }); + }); + }); + + describe('getAllTimeline', () => { + let mockFindSavedObject: jest.Mock; + let mockRequest: FrameworkRequest; + const pageInfo = { + pageSize: 10, + pageIndex: 1, + }; + let result = (null as unknown) as AllTimelinesResponse; + beforeEach(async () => { + (convertSavedObjectToSavedTimeline as jest.Mock).mockReturnValue(mockGetTimelineValue); + mockFindSavedObject = jest + .fn() + .mockResolvedValueOnce({ saved_objects: [mockSavedObject], total: 1 }) + .mockResolvedValueOnce({ saved_objects: [], total: 0 }) + .mockResolvedValueOnce({ saved_objects: [mockSavedObject], total: 1 }) + .mockResolvedValueOnce({ saved_objects: [mockSavedObject], total: 1 }) + .mockResolvedValue({ saved_objects: [], total: 0 }); + mockRequest = ({ + user: { + username: 'username', + }, + context: { + core: { + savedObjects: { + client: { + find: mockFindSavedObject, + }, + }, + }, + }, + } as unknown) as FrameworkRequest; + + result = await getAllTimeline(mockRequest, false, pageInfo, null, null, null, null); + }); + + afterEach(() => { + mockFindSavedObject.mockClear(); + (getNotesByTimelineId as jest.Mock).mockClear(); + (getAllPinnedEventsByTimelineId as jest.Mock).mockClear(); + }); + + test('should send correct options if no filters applys', async () => { + expect(mockFindSavedObject.mock.calls[0][0]).toEqual({ + filter: 'not siem-ui-timeline.attributes.status: draft', + page: pageInfo.pageIndex, + perPage: pageInfo.pageSize, + type: 'siem-ui-timeline', + sortField: undefined, + sortOrder: undefined, + search: undefined, + searchFields: ['title', 'description'], + }); + }); + + test('should send correct options for counts of default timelines', async () => { + expect(mockFindSavedObject.mock.calls[1][0]).toEqual({ + filter: + 'not siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and not siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options for counts of timeline templates', async () => { + expect(mockFindSavedObject.mock.calls[2][0]).toEqual({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options for counts of Elastic prebuilt templates', async () => { + expect(mockFindSavedObject.mock.calls[3][0]).toEqual({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options for counts of custom templates', async () => { + expect(mockFindSavedObject.mock.calls[4][0]).toEqual({ + filter: + 'siem-ui-timeline.attributes.timelineType: template and not siem-ui-timeline.attributes.status: draft and not siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + type: 'siem-ui-timeline', + }); + }); + + test('should send correct options for counts of favorite timeline', async () => { + expect(mockFindSavedObject.mock.calls[5][0]).toEqual({ + filter: + 'not siem-ui-timeline.attributes.status: draft and not siem-ui-timeline.attributes.status: immutable', + page: 1, + perPage: 1, + search: ' dXNlcm5hbWU=', + searchFields: ['title', 'description', 'favorite.keySearch'], + type: 'siem-ui-timeline', + }); + }); + + test('should call getNotesByTimelineId', async () => { + expect((getNotesByTimelineId as jest.Mock).mock.calls[0][1]).toEqual(mockSavedObject.id); + }); + + test('should call getAllPinnedEventsByTimelineId', async () => { + expect((getAllPinnedEventsByTimelineId as jest.Mock).mock.calls[0][1]).toEqual( + mockSavedObject.id + ); + }); + + test('should retuen correct result', async () => { + expect(result).toEqual({ + totalCount: 1, + customTemplateTimelineCount: 0, + defaultTimelineCount: 0, + elasticTemplateTimelineCount: 1, + favoriteCount: 0, + templateTimelineCount: 1, + timeline: [ + { + ...mockGetTimelineValue, + noteIds: [], + pinnedEventIds: [], + eventIdToNoteIds: [], + favorite: [], + notes: [], + pinnedEventsSaveObject: [], + }, + ], + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts index 6bc0ca64ae33fb..23ea3e6213469f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts @@ -41,7 +41,7 @@ interface ResponseTimelines { totalCount: number; } -interface AllTimelinesResponse extends ResponseTimelines { +export interface AllTimelinesResponse extends ResponseTimelines { defaultTimelineCount: number; templateTimelineCount: number; elasticTemplateTimelineCount: number; @@ -63,7 +63,7 @@ export interface Timeline { getAllTimeline: ( request: FrameworkRequest, onlyUserFavorite: boolean | null, - pageInfo: PageInfoTimeline | null, + pageInfo: PageInfoTimeline, search: string | null, sort: SortTimeline | null, status: TimelineStatusLiteralWithNull, @@ -152,17 +152,18 @@ const getTimelineTypeFilter = ( export const getExistingPrepackagedTimelines = async ( request: FrameworkRequest, countsOnly?: boolean, - pageInfo?: PageInfoTimeline | null + pageInfo?: PageInfoTimeline ): Promise<{ totalCount: number; timeline: TimelineSavedObject[]; }> => { - const queryPageInfo = countsOnly - ? { - perPage: 1, - page: 1, - } - : pageInfo ?? {}; + const queryPageInfo = + countsOnly && pageInfo == null + ? { + perPage: 1, + page: 1, + } + : { perPage: pageInfo?.pageSize, page: pageInfo?.pageIndex } ?? {}; const elasticTemplateTimelineOptions = { type: timelineSavedObjectType, ...queryPageInfo, @@ -175,7 +176,7 @@ export const getExistingPrepackagedTimelines = async ( export const getAllTimeline = async ( request: FrameworkRequest, onlyUserFavorite: boolean | null, - pageInfo: PageInfoTimeline | null, + pageInfo: PageInfoTimeline, search: string | null, sort: SortTimeline | null, status: TimelineStatusLiteralWithNull, @@ -183,13 +184,13 @@ export const getAllTimeline = async ( ): Promise => { const options: SavedObjectsFindOptions = { type: timelineSavedObjectType, - perPage: pageInfo?.pageSize ?? undefined, - page: pageInfo?.pageIndex ?? undefined, + perPage: pageInfo.pageSize, + page: pageInfo.pageIndex, search: search != null ? search : undefined, searchFields: onlyUserFavorite ? ['title', 'description', 'favorite.keySearch'] : ['title', 'description'], - filter: getTimelineTypeFilter(timelineType, status), + filter: getTimelineTypeFilter(timelineType ?? null, status ?? null), sortField: sort != null ? sort.sortField : undefined, sortOrder: sort != null ? sort.sortOrder : undefined, }; @@ -220,7 +221,7 @@ export const getAllTimeline = async ( searchFields: ['title', 'description', 'favorite.keySearch'], perPage: 1, page: 1, - filter: getTimelineTypeFilter(timelineType, TimelineStatus.active), + filter: getTimelineTypeFilter(timelineType ?? null, TimelineStatus.active), }; const result = await Promise.all([ @@ -496,7 +497,6 @@ const getAllSavedTimeline = async (request: FrameworkRequest, options: SavedObje ]); }) ); - return { totalCount: savedObjects.total, timeline: timelinesWithNotesAndPinnedEvents.map(([notes, pinnedEvents, timeline]) => @@ -532,14 +532,20 @@ export const timelineWithReduxProperties = ( pinnedEventsSaveObject: pinnedEvents, }); -export const getTimelines = async (request: FrameworkRequest, timelineIds?: string[] | null) => { +export const getSelectedTimelines = async ( + request: FrameworkRequest, + timelineIds?: string[] | null +) => { const savedObjectsClient = request.context.core.savedObjects.client; let exportedIds = timelineIds; if (timelineIds == null || timelineIds.length === 0) { const { timeline: savedAllTimelines } = await getAllTimeline( request, false, - null, + { + pageIndex: 1, + pageSize: timelineIds?.length ?? 0, + }, null, null, TimelineStatus.active, diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 37a97c03ad3328..000bd875930f9d 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -37,7 +37,7 @@ import { cleanDraftTimelinesRoute } from '../lib/timeline/routes/clean_draft_tim import { SetupPlugins } from '../plugin'; import { ConfigType } from '../config'; import { installPrepackedTimelinesRoute } from '../lib/timeline/routes/install_prepacked_timelines_route'; -import { getTimelineByIdRoute } from '../lib/timeline/routes/get_timeline_by_id_route'; +import { getTimelineRoute } from '../lib/timeline/routes/get_timeline_route'; export const initRoutes = ( router: IRouter, @@ -70,7 +70,7 @@ export const initRoutes = ( importTimelinesRoute(router, config, security); exportTimelinesRoute(router, config, security); getDraftTimelinesRoute(router, config, security); - getTimelineByIdRoute(router, config, security); + getTimelineRoute(router, config, security); cleanDraftTimelinesRoute(router, config, security); installPrepackedTimelinesRoute(router, config, security); From 8c6d655f17a6985d32943456442b77463c8ca0b2 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 3 Aug 2020 07:44:57 -0700 Subject: [PATCH 07/74] [Search] Set keep_alive parameter in async search (#73712) * [Search] Set keep_alive parameter in async search * Revert accidental change * Add batched_reduce_size Co-authored-by: Elastic Machine --- .../server/search/es_search_strategy.test.ts | 15 +++++++++++++++ .../server/search/es_search_strategy.ts | 11 +++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts index faa4f2ee499e51..4fd1e889ba1a51 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts @@ -113,4 +113,19 @@ describe('ES search strategy', () => { expect(method).toBe('POST'); expect(path).toBe('/foo-%E7%A8%8B/_rollup_search'); }); + + it('sets wait_for_completion_timeout and keep_alive in the request', async () => { + mockApiCaller.mockResolvedValueOnce(mockAsyncResponse); + + const params = { index: 'foo-*', body: {} }; + const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); + + await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); + + expect(mockApiCaller).toBeCalled(); + expect(mockApiCaller.mock.calls[0][0]).toBe('transport.request'); + const { query } = mockApiCaller.mock.calls[0][1]; + expect(query).toHaveProperty('wait_for_completion_timeout'); + expect(query).toHaveProperty('keep_alive'); + }); }); diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 358335a2a4d603..1f7b6a5f9aacea 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -80,8 +80,15 @@ async function asyncSearch( const method = request.id ? 'GET' : 'POST'; const path = encodeURI(request.id ? `/_async_search/${request.id}` : `/${index}/_async_search`); - // Wait up to 1s for the response to return - const query = toSnakeCase({ waitForCompletionTimeout: '100ms', ...queryParams }); + // Only report partial results every 64 shards; this should be reduced when we actually display partial results + const batchedReduceSize = request.id ? undefined : 64; + + const query = toSnakeCase({ + waitForCompletionTimeout: '100ms', // Wait up to 100ms for the response to return + keepAlive: '1m', // Extend the TTL for this search request by one minute + ...(batchedReduceSize && { batchedReduceSize }), + ...queryParams, + }); const { id, response, is_partial, is_running } = (await caller( 'transport.request', From 426651ec61c2004380ab9641d78ec69344199e9f Mon Sep 17 00:00:00 2001 From: Jimmy Kuang Date: Mon, 3 Aug 2020 08:05:15 -0700 Subject: [PATCH 08/74] Added this.name in replace of New Watch string - isNew Create else Save (#73982) * Added this.name in replace of New Watch string * Address precommit_hook fix * Run node scripts/precommit_hook --fix * Run node scripts/precommit_hook --fix * Run node scripts/precommit_hook --fix * Update watch.isNew logic, address xpack jest check, and fix precommit hook * Fix elint issues * Used Translation - removed xpack.watcher.models.baseWatch.displayName and xpack.securitySolution.auditd.failedxLoginTooManyTimes * Used Translation - removed xpack.watcher.models.baseWatch.displayName * Updated SaveWatch function - separate create and saved displayName and Eslint fix * Added back xpack.securitySolution.auditd.failedxLoginTooManyTimesDescription * Reverse xpack.securitySolution.auditd.inDescription back to in * Remove x typo in failedx Co-authored-by: Elastic Machine --- .../translations/translations/ja-JP.json | 3 +-- .../translations/translations/zh-CN.json | 1 - .../application/models/watch/base_watch.js | 6 +----- .../sections/watch_edit/watch_edit_actions.ts | 19 +++++++++++++------ 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e2f59f3fa910a3..f13aa374e549ca 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19304,8 +19304,7 @@ "xpack.watcher.models.baseAction.simulateMessage": "アクション {id} のシミュレーションが完了しました", "xpack.watcher.models.baseAction.typeName": "アクション", "xpack.watcher.models.baseWatch.createUnknownActionTypeErrorMessage": "不明なアクションタイプ {type} を作成しようとしました。", - "xpack.watcher.models.baseWatch.displayName": "新規ウォッチ", - "xpack.watcher.models.baseWatch.idPropertyMissingBadRequestMessage": "json 引数には {id} プロパティが含まれている必要があります", + "xpack.watcher.models.baseWatch.idPropertyMissingBadRequestMessage": "json 引数には {id} プロパティが含まれている必要があります", "xpack.watcher.models.baseWatch.selectMessageText": "新規ウォッチをセットアップします。", "xpack.watcher.models.baseWatch.typeName": "ウォッチ", "xpack.watcher.models.baseWatch.watchJsonPropertyMissingBadRequestMessage": "json 引数には {watchJson} プロパティが含まれている必要があります", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 316d3247d19d5e..83e290b8c241ad 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -19311,7 +19311,6 @@ "xpack.watcher.models.baseAction.simulateMessage": "已成功模拟操作 {id}", "xpack.watcher.models.baseAction.typeName": "操作", "xpack.watcher.models.baseWatch.createUnknownActionTypeErrorMessage": "尝试创建的操作类型 {type} 未知。", - "xpack.watcher.models.baseWatch.displayName": "新建监视", "xpack.watcher.models.baseWatch.idPropertyMissingBadRequestMessage": "json 参数必须包含 {id} 属性", "xpack.watcher.models.baseWatch.selectMessageText": "设置新监视。", "xpack.watcher.models.baseWatch.typeName": "监视", diff --git a/x-pack/plugins/watcher/public/application/models/watch/base_watch.js b/x-pack/plugins/watcher/public/application/models/watch/base_watch.js index eaced3e27c8a01..6b7d693bb308e5 100644 --- a/x-pack/plugins/watcher/public/application/models/watch/base_watch.js +++ b/x-pack/plugins/watcher/public/application/models/watch/base_watch.js @@ -79,11 +79,7 @@ export class BaseWatch { }; get displayName() { - if (this.isNew) { - return i18n.translate('xpack.watcher.models.baseWatch.displayName', { - defaultMessage: 'New Watch', - }); - } else if (this.name) { + if (this.name) { return this.name; } else { return this.id; diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/watch_edit_actions.ts b/x-pack/plugins/watcher/public/application/sections/watch_edit/watch_edit_actions.ts index 2d62bca75c1a1e..36dfdb55b4ab63 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/watch_edit_actions.ts +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/watch_edit_actions.ts @@ -66,12 +66,19 @@ export async function saveWatch(watch: BaseWatch, toasts: ToastsSetup): Promise< try { await createWatch(watch); toasts.addSuccess( - i18n.translate('xpack.watcher.sections.watchEdit.json.saveSuccessNotificationText', { - defaultMessage: "Saved '{watchDisplayName}'", - values: { - watchDisplayName: watch.displayName, - }, - }) + watch.isNew + ? i18n.translate('xpack.watcher.sections.watchEdit.json.createSuccessNotificationText', { + defaultMessage: "Created '{watchDisplayName}'", + values: { + watchDisplayName: watch.displayName, + }, + }) + : i18n.translate('xpack.watcher.sections.watchEdit.json.saveSuccessNotificationText', { + defaultMessage: "Saved '{watchDisplayName}'", + values: { + watchDisplayName: watch.displayName, + }, + }) ); goToWatchList(); } catch (error) { From a25feb6d575aabb4bb1ca0662fc5552c999f33f5 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Mon, 3 Aug 2020 17:07:33 +0200 Subject: [PATCH 09/74] [ML] Functional tests - stabilize waiting for AD, DFA, TFM job list refresh (#74064) With this PR, the job list refresh for anomaly detection, data frame analytics and transforms is waiting for the refresh loading indicator to disappear before moving on. --- .../refresh_analytics_list_button.tsx | 2 +- .../refresh_jobs_list_button.js | 2 +- .../refresh_transform_list_button.tsx | 2 +- .../services/ml/data_frame_analytics_table.ts | 9 ++++++++- x-pack/test/functional/services/ml/job_table.ts | 9 ++++++++- .../services/transform/transform_table.ts | 13 ++++++++++++- 6 files changed, 31 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx index f54cc4621eccfe..e988640d2eae51 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx @@ -15,7 +15,7 @@ export const RefreshAnalyticsListButton: FC = () => { const { refresh } = useRefreshAnalyticsList({ isLoading: setIsLoading }); return ( diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js index ecb86268678875..7c55ed01f432a6 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; export const RefreshJobsListButton = ({ onRefreshClick, isRefreshing }) => ( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx index f8a1f84937326b..f886b0b4614827 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx @@ -20,7 +20,7 @@ export const RefreshTransformListButton: FC = ({ diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts index f452c9cce7a1aa..d315f9eb772104 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts @@ -62,8 +62,15 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F return rows; } + public async waitForRefreshButtonLoaded() { + await testSubjects.existOrFail('~mlAnalyticsRefreshListButton', { timeout: 10 * 1000 }); + await testSubjects.existOrFail('mlAnalyticsRefreshListButton loaded', { timeout: 30 * 1000 }); + } + public async refreshAnalyticsTable() { - await testSubjects.click('mlAnalyticsRefreshListButton'); + await this.waitForRefreshButtonLoaded(); + await testSubjects.click('~mlAnalyticsRefreshListButton'); + await this.waitForRefreshButtonLoaded(); await this.waitForAnalyticsToLoad(); } diff --git a/x-pack/test/functional/services/ml/job_table.ts b/x-pack/test/functional/services/ml/job_table.ts index a72d9c204060bd..58a1afad88e111 100644 --- a/x-pack/test/functional/services/ml/job_table.ts +++ b/x-pack/test/functional/services/ml/job_table.ts @@ -141,8 +141,15 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte }); } + public async waitForRefreshButtonLoaded() { + await testSubjects.existOrFail('~mlRefreshJobListButton', { timeout: 10 * 1000 }); + await testSubjects.existOrFail('mlRefreshJobListButton loaded', { timeout: 30 * 1000 }); + } + public async refreshJobList() { - await testSubjects.click('mlRefreshJobListButton'); + await this.waitForRefreshButtonLoaded(); + await testSubjects.click('~mlRefreshJobListButton'); + await this.waitForRefreshButtonLoaded(); await this.waitForJobsToLoad(); } diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 453dca904b6059..37d8b6e51072ff 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -95,8 +95,19 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { }); } + public async waitForRefreshButtonLoaded() { + await testSubjects.existOrFail('~transformRefreshTransformListButton', { + timeout: 10 * 1000, + }); + await testSubjects.existOrFail('transformRefreshTransformListButton loaded', { + timeout: 30 * 1000, + }); + } + public async refreshTransformList() { - await testSubjects.click('transformRefreshTransformListButton'); + await this.waitForRefreshButtonLoaded(); + await testSubjects.click('~transformRefreshTransformListButton'); + await this.waitForRefreshButtonLoaded(); await this.waitForTransformsToLoad(); } From 346bab35a36d3f46780fafc1390bb30d4ff5eca1 Mon Sep 17 00:00:00 2001 From: Eric Davis Date: Mon, 3 Aug 2020 11:17:07 -0400 Subject: [PATCH 10/74] Kibana issue #73932 - enrollment flyout changes (#74008) Co-authored-by: Elastic Machine --- .../components/enrollment_instructions/manual/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx index fe11c4cb08d13e..a77de9369277b8 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx @@ -32,7 +32,7 @@ export const ManualInstructions: React.FunctionComponent = ({ const macOsLinuxTarCommand = `./elastic-agent enroll ${enrollArgs} ./elastic-agent run`; - const linuxDebRpmCommand = `./elastic-agent enroll ${enrollArgs} + const linuxDebRpmCommand = `elastic-agent enroll ${enrollArgs} systemctl enable elastic-agent systemctl start elastic-agent`; @@ -44,7 +44,7 @@ systemctl start elastic-agent`; From 234e6f130d40909a74da5a14672349e929b65599 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 3 Aug 2020 09:37:30 -0600 Subject: [PATCH 11/74] change 'Add from Visualize library' button text to 'Add from Kibana' (#74002) Co-authored-by: Elastic Machine --- x-pack/plugins/canvas/i18n/components.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index 03d6ade7bea692..71e3386d821f19 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -15,7 +15,7 @@ export const ComponentStrings = { }), getTitleText: () => i18n.translate('xpack.canvas.embedObject.titleText', { - defaultMessage: 'Add from Visualize library', + defaultMessage: 'Add from Kibana', }), }, AdvancedFilter: { @@ -1308,7 +1308,7 @@ export const ComponentStrings = { }), getEmbedObjectMenuItemLabel: () => i18n.translate('xpack.canvas.workpadHeaderElementMenu.embedObjectMenuItemLabel', { - defaultMessage: 'Add from Visualize library', + defaultMessage: 'Add from Kibana', }), getFilterMenuItemLabel: () => i18n.translate('xpack.canvas.workpadHeaderElementMenu.filterMenuItemLabel', { From b9e5ae9c777f6e4d2007fd4abb315cc1b05eafd2 Mon Sep 17 00:00:00 2001 From: Dan Panzarella Date: Mon, 3 Aug 2020 11:53:52 -0400 Subject: [PATCH 12/74] [Security Solution] Filter endpoint hosts by agent status (#71882) --- .../server/endpoint/routes/metadata/index.ts | 30 +++++- .../endpoint/routes/metadata/metadata.test.ts | 51 +++++++++- .../routes/metadata/query_builders.test.ts | 4 +- .../routes/metadata/query_builders.ts | 76 ++++++++------- .../metadata/support/agent_status.test.ts | 96 +++++++++++++++++++ .../routes/metadata/support/agent_status.ts | 47 +++++++++ .../apis/metadata.ts | 30 ++++-- 7 files changed, 286 insertions(+), 48 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 084f892369b519..161a31e2ec9343 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -21,6 +21,7 @@ import { EndpointAppContext } from '../../types'; import { AgentService } from '../../../../../ingest_manager/server'; import { Agent, AgentStatus } from '../../../../../ingest_manager/common/types/models'; import { findAllUnenrolledAgentIds } from './support/unenroll'; +import { findAgentIDsByStatus } from './support/agent_status'; interface HitSource { _source: HostMetadata; @@ -52,6 +53,21 @@ const getLogger = (endpointAppContext: EndpointAppContext): Logger => { return endpointAppContext.logFactory.get('metadata'); }; +/* Filters that can be applied to the endpoint fetch route */ +export const endpointFilters = schema.object({ + kql: schema.nullable(schema.string()), + host_status: schema.nullable( + schema.arrayOf( + schema.oneOf([ + schema.literal(HostStatus.ONLINE.toString()), + schema.literal(HostStatus.OFFLINE.toString()), + schema.literal(HostStatus.UNENROLLING.toString()), + schema.literal(HostStatus.ERROR.toString()), + ]) + ) + ), +}); + export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { const logger = getLogger(endpointAppContext); router.post( @@ -76,10 +92,7 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp ]) ) ), - /** - * filter to be applied, it could be a kql expression or discrete filter to be implemented - */ - filter: schema.nullable(schema.oneOf([schema.string()])), + filters: endpointFilters, }) ), }, @@ -103,12 +116,21 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp context.core.savedObjects.client ); + const statusIDs = req.body?.filters?.host_status?.length + ? await findAgentIDsByStatus( + agentService, + context.core.savedObjects.client, + req.body?.filters?.host_status + ) + : undefined; + const queryParams = await kibanaRequestToMetadataListESQuery( req, endpointAppContext, metadataIndexPattern, { unenrolledAgentIds: unenrolledAgentIds.concat(IGNORED_ELASTIC_AGENT_IDS), + statusAgentIDs: statusIDs, } ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index f3b832de9a7862..29624b35d5c9e0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -27,7 +27,7 @@ import { HostStatus, } from '../../../../common/endpoint/types'; import { SearchResponse } from 'elasticsearch'; -import { registerEndpointRoutes } from './index'; +import { registerEndpointRoutes, endpointFilters } from './index'; import { createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, @@ -170,7 +170,7 @@ describe('test endpoint route', () => { }, ], - filter: 'not host.ip:10.140.73.246', + filters: { kql: 'not host.ip:10.140.73.246' }, }, }); @@ -395,6 +395,53 @@ describe('test endpoint route', () => { }); }); +describe('Filters Schema Test', () => { + it('accepts a single host status', () => { + expect( + endpointFilters.validate({ + host_status: ['error'], + }) + ).toBeTruthy(); + }); + + it('accepts multiple host status filters', () => { + expect( + endpointFilters.validate({ + host_status: ['offline', 'unenrolling'], + }) + ).toBeTruthy(); + }); + + it('rejects invalid statuses', () => { + expect(() => + endpointFilters.validate({ + host_status: ['foobar'], + }) + ).toThrowError(); + }); + + it('accepts a KQL string', () => { + expect( + endpointFilters.validate({ + kql: 'whatever.field', + }) + ).toBeTruthy(); + }); + + it('accepts KQL + status', () => { + expect( + endpointFilters.validate({ + kql: 'thing.var', + host_status: ['online'], + }) + ).toBeTruthy(); + }); + + it('accepts no filters', () => { + expect(endpointFilters.validate({})).toBeTruthy(); + }); +}); + function createSearchResponse(hostMetadata?: HostMetadata): SearchResponse { return ({ took: 15, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts index 266d522e8a41de..e9eb7093a76314 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts @@ -127,7 +127,7 @@ describe('query builder', () => { it('test default query params for all endpoints metadata when body filter is provided', async () => { const mockRequest = httpServerMock.createKibanaRequest({ body: { - filter: 'not host.ip:10.140.73.246', + filters: { kql: 'not host.ip:10.140.73.246' }, }, }); const query = await kibanaRequestToMetadataListESQuery( @@ -201,7 +201,7 @@ describe('query builder', () => { const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672'; const mockRequest = httpServerMock.createKibanaRequest({ body: { - filter: 'not host.ip:10.140.73.246', + filters: { kql: 'not host.ip:10.140.73.246' }, }, }); const query = await kibanaRequestToMetadataListESQuery( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index f6385d27100479..ba9be96201dbe8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -9,6 +9,7 @@ import { EndpointAppContext } from '../../types'; export interface QueryBuilderOptions { unenrolledAgentIds?: string[]; + statusAgentIDs?: string[]; } export async function kibanaRequestToMetadataListESQuery( @@ -22,7 +23,11 @@ export async function kibanaRequestToMetadataListESQuery( const pagingProperties = await getPagingProperties(request, endpointAppContext); return { body: { - query: buildQueryBody(request, queryBuilderOptions?.unenrolledAgentIds!), + query: buildQueryBody( + request, + queryBuilderOptions?.unenrolledAgentIds!, + queryBuilderOptions?.statusAgentIDs! + ), collapse: { field: 'host.id', inner_hits: { @@ -76,47 +81,52 @@ async function getPagingProperties( function buildQueryBody( // eslint-disable-next-line @typescript-eslint/no-explicit-any request: KibanaRequest, - unerolledAgentIds: string[] | undefined + unerolledAgentIds: string[] | undefined, + statusAgentIDs: string[] | undefined // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Record { - const filterUnenrolledAgents = unerolledAgentIds && unerolledAgentIds.length > 0; - if (typeof request?.body?.filter === 'string') { - const kqlQuery = esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(request.body.filter)); - return { - bool: { - must: filterUnenrolledAgents - ? [ - { - bool: { - must_not: { - terms: { - 'elastic.agent.id': unerolledAgentIds, - }, - }, - }, - }, - { - ...kqlQuery, - }, - ] - : [ - { - ...kqlQuery, - }, - ], - }, - }; - } - return filterUnenrolledAgents - ? { - bool: { + const filterUnenrolledAgents = + unerolledAgentIds && unerolledAgentIds.length > 0 + ? { must_not: { terms: { 'elastic.agent.id': unerolledAgentIds, }, }, + } + : null; + const filterStatusAgents = statusAgentIDs + ? { + must: { + terms: { + 'elastic.agent.id': statusAgentIDs, + }, }, } + : null; + + const idFilter = { + bool: { + ...filterUnenrolledAgents, + ...filterStatusAgents, + }, + }; + + if (request?.body?.filters?.kql) { + const kqlQuery = esKuery.toElasticsearchQuery( + esKuery.fromKueryExpression(request.body.filters.kql) + ); + const q = []; + if (filterUnenrolledAgents || filterStatusAgents) { + q.push(idFilter); + } + q.push({ ...kqlQuery }); + return { + bool: { must: q }, + }; + } + return filterUnenrolledAgents || filterStatusAgents + ? idFilter : { match_all: {}, }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts new file mode 100644 index 00000000000000..a4b6b0750ec10d --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import { findAgentIDsByStatus } from './agent_status'; +import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks'; +import { AgentService } from '../../../../../../ingest_manager/server/services'; +import { createMockAgentService } from '../../../mocks'; +import { Agent } from '../../../../../../ingest_manager/common/types/models'; +import { AgentStatusKueryHelper } from '../../../../../../ingest_manager/common/services'; + +describe('test filtering endpoint hosts by agent status', () => { + let mockSavedObjectClient: jest.Mocked; + let mockAgentService: jest.Mocked; + beforeEach(() => { + mockSavedObjectClient = savedObjectsClientMock.create(); + mockAgentService = createMockAgentService(); + }); + + it('will accept a valid status condition', async () => { + mockAgentService.listAgents.mockImplementationOnce(() => + Promise.resolve({ + agents: [], + total: 0, + page: 1, + perPage: 10, + }) + ); + + const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, ['online']); + expect(result).toBeDefined(); + }); + + it('will filter for offline hosts', async () => { + mockAgentService.listAgents + .mockImplementationOnce(() => + Promise.resolve({ + agents: [({ id: 'id1' } as unknown) as Agent, ({ id: 'id2' } as unknown) as Agent], + total: 2, + page: 1, + perPage: 2, + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + agents: [], + total: 2, + page: 2, + perPage: 2, + }) + ); + + const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, ['offline']); + const offlineKuery = AgentStatusKueryHelper.buildKueryForOfflineAgents(); + expect(mockAgentService.listAgents.mock.calls[0][1].kuery).toEqual( + expect.stringContaining(offlineKuery) + ); + expect(result).toBeDefined(); + expect(result).toEqual(['id1', 'id2']); + }); + + it('will filter for multiple statuses', async () => { + mockAgentService.listAgents + .mockImplementationOnce(() => + Promise.resolve({ + agents: [({ id: 'A' } as unknown) as Agent, ({ id: 'B' } as unknown) as Agent], + total: 2, + page: 1, + perPage: 2, + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + agents: [], + total: 2, + page: 2, + perPage: 2, + }) + ); + + const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, [ + 'unenrolling', + 'error', + ]); + const unenrollKuery = AgentStatusKueryHelper.buildKueryForUnenrollingAgents(); + const errorKuery = AgentStatusKueryHelper.buildKueryForErrorAgents(); + expect(mockAgentService.listAgents.mock.calls[0][1].kuery).toEqual( + expect.stringContaining(`${unenrollKuery} OR ${errorKuery}`) + ); + expect(result).toBeDefined(); + expect(result).toEqual(['A', 'B']); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts new file mode 100644 index 00000000000000..86f6d1a9a65e22 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import { AgentService } from '../../../../../../ingest_manager/server'; +import { AgentStatusKueryHelper } from '../../../../../../ingest_manager/common/services'; +import { Agent } from '../../../../../../ingest_manager/common/types/models'; +import { HostStatus } from '../../../../../common/endpoint/types'; + +const STATUS_QUERY_MAP = new Map([ + [HostStatus.ONLINE.toString(), AgentStatusKueryHelper.buildKueryForOnlineAgents()], + [HostStatus.OFFLINE.toString(), AgentStatusKueryHelper.buildKueryForOfflineAgents()], + [HostStatus.ERROR.toString(), AgentStatusKueryHelper.buildKueryForErrorAgents()], + [HostStatus.UNENROLLING.toString(), AgentStatusKueryHelper.buildKueryForUnenrollingAgents()], +]); + +export async function findAgentIDsByStatus( + agentService: AgentService, + soClient: SavedObjectsClientContract, + status: string[], + pageSize: number = 1000 +): Promise { + const helpers = status.map((s) => STATUS_QUERY_MAP.get(s)); + const searchOptions = (pageNum: number) => { + return { + page: pageNum, + perPage: pageSize, + showInactive: true, + kuery: `(fleet-agents.packages : "endpoint" AND (${helpers.join(' OR ')}))`, + }; + }; + + let page = 1; + + const result: string[] = []; + let hasMore = true; + + while (hasMore) { + const agents = await agentService.listAgents(soClient, searchOptions(page++)); + result.push(...agents.agents.map((agent: Agent) => agent.id)); + hasMore = agents.agents.length > 0; + } + return result; +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 719327e5f9b79b..3afa9f397a2eaf 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -119,7 +119,11 @@ export default function ({ getService }: FtrProviderContext) { const { body } = await supertest .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') - .send({ filter: 'not host.ip:10.46.229.234' }) + .send({ + filters: { + kql: 'not host.ip:10.46.229.234', + }, + }) .expect(200); expect(body.total).to.eql(2); expect(body.hosts.length).to.eql(2); @@ -141,7 +145,9 @@ export default function ({ getService }: FtrProviderContext) { page_index: 0, }, ], - filter: `not host.ip:${notIncludedIp}`, + filters: { + kql: `not host.ip:${notIncludedIp}`, + }, }) .expect(200); expect(body.total).to.eql(2); @@ -166,7 +172,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `host.os.Ext.variant:${variantValue}`, + filters: { + kql: `host.os.Ext.variant:${variantValue}`, + }, }) .expect(200); expect(body.total).to.eql(2); @@ -185,7 +193,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `host.ip:${targetEndpointIp}`, + filters: { + kql: `host.ip:${targetEndpointIp}`, + }, }) .expect(200); expect(body.total).to.eql(1); @@ -204,7 +214,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `not Endpoint.policy.applied.status:success`, + filters: { + kql: `not Endpoint.policy.applied.status:success`, + }, }) .expect(200); const statuses: Set = new Set( @@ -223,7 +235,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `elastic.agent.id:${targetElasticAgentId}`, + filters: { + kql: `elastic.agent.id:${targetElasticAgentId}`, + }, }) .expect(200); expect(body.total).to.eql(1); @@ -243,7 +257,9 @@ export default function ({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: '', + filters: { + kql: '', + }, }) .expect(200); expect(body.total).to.eql(numberOfHostsInFixture); From 5cabe9c95dd0e7ae2e6181c2d062268eb3fef601 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Mon, 3 Aug 2020 10:57:32 -0500 Subject: [PATCH 13/74] [Ingest Manager] improve error handling of package install (#73728) * refactor to add assets refs to SO before install * get ingest pipeline refs by looping through datasets because names are diff than path dir * fix bug and improve error handling * bump package number from merge conflict * add integration test for the epm-package saved object * accidentally pasted line of code * rename errors for consistency * pass custom error when IngestManagerError * rename package from outdated to update --- .../plugins/ingest_manager/server/errors.ts | 8 +++ .../server/routes/epm/handlers.ts | 21 ++++--- .../elasticsearch/ingest_pipeline/install.ts | 49 +++++++++------ .../epm/elasticsearch/template/install.ts | 18 +++--- .../services/epm/kibana/assets/install.ts | 59 +++++++----------- .../server/services/epm/packages/install.ts | 47 +++++++++------ .../server/services/epm/packages/remove.ts | 6 +- .../server/services/epm/registry/index.ts | 3 +- .../apis/epm/index.js | 17 ++++++ .../apis/epm/install_errors.ts | 51 ++++++++++++++++ .../apis/epm/install_remove_assets.ts | 60 +++++++++++++++++++ .../apis/epm/list.ts | 2 +- .../0.1.0/dataset/test/fields/fields.yml | 16 +++++ .../update/0.1.0/dataset/test/manifest.yml | 9 +++ .../test_packages/update/0.1.0/docs/README.md | 3 + .../test_packages/update/0.1.0/manifest.yml | 20 +++++++ .../0.2.0/dataset/test/fields/fields.yml | 16 +++++ .../update/0.2.0/dataset/test/manifest.yml | 9 +++ .../test_packages/update/0.2.0/docs/README.md | 3 + .../test_packages/update/0.2.0/manifest.yml | 20 +++++++ .../apis/index.js | 7 +-- 21 files changed, 343 insertions(+), 101 deletions(-) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/epm/index.js create mode 100644 x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/docs/README.md create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/docs/README.md create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml diff --git a/x-pack/plugins/ingest_manager/server/errors.ts b/x-pack/plugins/ingest_manager/server/errors.ts index ee03b3faf79d13..401211409ebf75 100644 --- a/x-pack/plugins/ingest_manager/server/errors.ts +++ b/x-pack/plugins/ingest_manager/server/errors.ts @@ -15,9 +15,17 @@ export class IngestManagerError extends Error { export const getHTTPResponseCode = (error: IngestManagerError): number => { if (error instanceof RegistryError) { return 502; // Bad Gateway + } + if (error instanceof PackageNotFoundError) { + return 404; + } + if (error instanceof PackageOutdatedError) { + return 400; } else { return 400; // Bad Request } }; export class RegistryError extends IngestManagerError {} +export class PackageNotFoundError extends IngestManagerError {} +export class PackageOutdatedError extends IngestManagerError {} diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index f54e61280b98ad..f47234fb201182 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -32,6 +32,7 @@ import { getLimitedPackages, getInstallationObject, } from '../../services/epm/packages'; +import { IngestManagerError, getHTTPResponseCode } from '../../errors'; export const getCategoriesHandler: RequestHandler< undefined, @@ -165,23 +166,25 @@ export const installPackageHandler: RequestHandler isPipeline(path)); - if (datasets) { - const pipelines = datasets.reduce>>((acc, dataset) => { - if (dataset.ingest_pipeline) { - acc.push( - installPipelinesForDataset({ - dataset, - callCluster, - paths: pipelinePaths, - pkgVersion: registryPackage.version, - }) - ); - } - return acc; - }, []); - const pipelinesToSave = await Promise.all(pipelines).then((results) => results.flat()); - return saveInstalledEsRefs(savedObjectsClient, registryPackage.name, pipelinesToSave); - } - return []; + // get and save pipeline refs before installing pipelines + const pipelineRefs = datasets.reduce((acc, dataset) => { + const filteredPaths = pipelinePaths.filter((path) => isDatasetPipeline(path, dataset.path)); + const pipelineObjectRefs = filteredPaths.map((path) => { + const { name } = getNameAndExtension(path); + const nameForInstallation = getPipelineNameForInstallation({ + pipelineName: name, + dataset, + packageVersion: registryPackage.version, + }); + return { id: nameForInstallation, type: ElasticsearchAssetType.ingestPipeline }; + }); + acc.push(...pipelineObjectRefs); + return acc; + }, []); + await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, pipelineRefs); + const pipelines = datasets.reduce>>((acc, dataset) => { + if (dataset.ingest_pipeline) { + acc.push( + installPipelinesForDataset({ + dataset, + callCluster, + paths: pipelinePaths, + pkgVersion: registryPackage.version, + }) + ); + } + return acc; + }, []); + return await Promise.all(pipelines).then((results) => results.flat()); }; export function rewriteIngestPipeline( 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 436a6a1bdc55d7..2a3120f064904a 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 @@ -41,6 +41,16 @@ export const installTemplates = async ( ); // build templates per dataset from yml files const datasets = registryPackage.datasets; + if (!datasets) return []; + // get template refs to save + const installedTemplateRefs = datasets.map((dataset) => ({ + id: generateTemplateName(dataset), + type: ElasticsearchAssetType.indexTemplate, + })); + + // add package installation's references to index templates + await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, installedTemplateRefs); + if (datasets) { const installTemplatePromises = datasets.reduce>>((acc, dataset) => { acc.push( @@ -55,14 +65,6 @@ export const installTemplates = async ( const res = await Promise.all(installTemplatePromises); const installedTemplates = res.flat(); - // get template refs to save - const installedTemplateRefs = installedTemplates.map((template) => ({ - id: template.templateName, - type: ElasticsearchAssetType.indexTemplate, - })); - - // add package installation's references to index templates - await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, installedTemplateRefs); return installedTemplates; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts index a3fe444b19b1a7..5741764164b839 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts @@ -11,14 +11,8 @@ import { } from 'src/core/server'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; import * as Registry from '../../registry'; -import { - AssetType, - KibanaAssetType, - AssetReference, - KibanaAssetReference, -} from '../../../../types'; -import { deleteKibanaSavedObjectsAssets } from '../../packages/remove'; -import { getInstallationObject, savedObjectTypes } from '../../packages'; +import { AssetType, KibanaAssetType, AssetReference } from '../../../../types'; +import { savedObjectTypes } from '../../packages'; type SavedObjectToBe = Required & { type: AssetType }; export type ArchiveAsset = Pick< @@ -28,7 +22,7 @@ export type ArchiveAsset = Pick< type: AssetType; }; -export async function getKibanaAsset(key: string) { +export async function getKibanaAsset(key: string): Promise { const buffer = Registry.getAsset(key); // cache values are buffers. convert to string / JSON @@ -51,31 +45,18 @@ export function createSavedObjectKibanaAsset(asset: ArchiveAsset): SavedObjectTo export async function installKibanaAssets(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; - paths: string[]; + kibanaAssets: ArchiveAsset[]; isUpdate: boolean; -}): Promise { - const { savedObjectsClient, paths, pkgName, isUpdate } = options; - - if (isUpdate) { - // delete currently installed kibana saved objects and installation references - const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const installedKibanaRefs = installedPkg?.attributes.installed_kibana; - - if (installedKibanaRefs?.length) { - await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedKibanaRefs); - await deleteKibanaInstalledRefs(savedObjectsClient, pkgName, installedKibanaRefs); - } - } +}): Promise { + const { savedObjectsClient, kibanaAssets } = options; - // install the new assets and save installation references + // install the assets const kibanaAssetTypes = Object.values(KibanaAssetType); const installedAssets = await Promise.all( kibanaAssetTypes.map((assetType) => - installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) + installKibanaSavedObjects({ savedObjectsClient, assetType, kibanaAssets }) ) ); - // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] - // call .flat to flatten into one dimensional array return installedAssets.flat(); } export const deleteKibanaInstalledRefs = async ( @@ -92,21 +73,25 @@ export const deleteKibanaInstalledRefs = async ( installed_kibana: installedAssetsToSave, }); }; - +export async function getKibanaAssets(paths: string[]) { + const isKibanaAssetType = (path: string) => Registry.pathParts(path).type in KibanaAssetType; + const filteredPaths = paths.filter(isKibanaAssetType); + const kibanaAssets = await Promise.all(filteredPaths.map((path) => getKibanaAsset(path))); + return kibanaAssets; +} async function installKibanaSavedObjects({ savedObjectsClient, assetType, - paths, + kibanaAssets, }: { savedObjectsClient: SavedObjectsClientContract; assetType: KibanaAssetType; - paths: string[]; + kibanaAssets: ArchiveAsset[]; }) { - const isSameType = (path: string) => assetType === Registry.pathParts(path).type; - const pathsOfType = paths.filter((path) => isSameType(path)); - const kibanaAssets = await Promise.all(pathsOfType.map((path) => getKibanaAsset(path))); + const isSameType = (asset: ArchiveAsset) => assetType === asset.type; + const filteredKibanaAssets = kibanaAssets.filter((asset) => isSameType(asset)); const toBeSavedObjects = await Promise.all( - kibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) + filteredKibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) ); if (toBeSavedObjects.length === 0) { @@ -115,13 +100,11 @@ async function installKibanaSavedObjects({ const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { overwrite: true, }); - const createdObjects = createResults.saved_objects; - const installed = createdObjects.map(toAssetReference); - return installed; + return createResults.saved_objects; } } -function toAssetReference({ id, type }: SavedObject) { +export function toAssetReference({ id, type }: SavedObject) { const reference: AssetReference = { id, type: type as KibanaAssetType }; return reference; 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 a69daae6e04107..4d51689b872e19 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 @@ -5,7 +5,6 @@ */ import { SavedObjectsClientContract } from 'src/core/server'; -import Boom from 'boom'; import semver from 'semver'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { @@ -25,8 +24,15 @@ import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; import { installPipelines, deletePipelines } from '../elasticsearch/ingest_pipeline/'; import { installILMPolicy } from '../elasticsearch/ilm/install'; -import { installKibanaAssets } from '../kibana/assets/install'; +import { + installKibanaAssets, + getKibanaAssets, + toAssetReference, + ArchiveAsset, +} from '../kibana/assets/install'; import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; +import { deleteKibanaSavedObjectsAssets } from './remove'; +import { PackageOutdatedError } from '../../../errors'; export async function installLatestPackage(options: { savedObjectsClient: SavedObjectsClientContract; @@ -97,7 +103,7 @@ export async function installPackage(options: { // and be replaced by getPackageInfo after adjusting for it to not group/use archive assets const latestPackage = await Registry.fetchFindLatestPackage(pkgName); if (semver.lt(pkgVersion, latestPackage.version)) - throw Boom.badRequest('Cannot install or update to an out-of-date package'); + throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`); const paths = await Registry.getArchiveInfo(pkgName, pkgVersion); const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion); @@ -124,12 +130,23 @@ export async function installPackage(options: { toSaveESIndexPatterns, }); } - const installIndexPatternPromise = installIndexPatterns(savedObjectsClient, pkgName, pkgVersion); + const kibanaAssets = await getKibanaAssets(paths); + if (installedPkg) + await deleteKibanaSavedObjectsAssets( + savedObjectsClient, + installedPkg.attributes.installed_kibana + ); + // save new kibana refs before installing the assets + const installedKibanaAssetsRefs = await saveKibanaAssetsRefs( + savedObjectsClient, + pkgName, + kibanaAssets + ); const installKibanaAssetsPromise = installKibanaAssets({ savedObjectsClient, pkgName, - paths, + kibanaAssets, isUpdate, }); @@ -169,21 +186,14 @@ export async function installPackage(options: { ); } - // get template refs to save const installedTemplateRefs = installedTemplates.map((template) => ({ id: template.templateName, type: ElasticsearchAssetType.indexTemplate, })); - - const [installedKibanaAssets] = await Promise.all([ - installKibanaAssetsPromise, - installIndexPatternPromise, - ]); - - await saveInstalledKibanaRefs(savedObjectsClient, pkgName, installedKibanaAssets); + await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]); // update to newly installed version when all assets are successfully installed if (isUpdate) await updateVersion(savedObjectsClient, pkgName, pkgVersion); - return [...installedKibanaAssets, ...installedPipelines, ...installedTemplateRefs]; + return [...installedKibanaAssetsRefs, ...installedPipelines, ...installedTemplateRefs]; } const updateVersion = async ( savedObjectsClient: SavedObjectsClientContract, @@ -230,15 +240,16 @@ export async function createInstallation(options: { return [...installedKibana, ...installedEs]; } -export const saveInstalledKibanaRefs = async ( +export const saveKibanaAssetsRefs = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, - installedAssets: KibanaAssetReference[] + kibanaAssets: ArchiveAsset[] ) => { + const assetRefs = kibanaAssets.map(toAssetReference); await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - installed_kibana: installedAssets, + installed_kibana: assetRefs, }); - return installedAssets; + return assetRefs; }; export const saveInstalledEsRefs = async ( 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 81bc5847e6c0e5..1acf2131dcb010 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 @@ -102,10 +102,12 @@ async function deleteTemplate(callCluster: CallESAsCurrentUser, name: string): P export async function deleteKibanaSavedObjectsAssets( savedObjectsClient: SavedObjectsClientContract, - installedObjects: AssetReference[] + installedRefs: AssetReference[] ) { + if (!installedRefs.length) return; + const logger = appContextService.getLogger(); - const deletePromises = installedObjects.map(({ id, type }) => { + const deletePromises = installedRefs.map(({ id, type }) => { const assetType = type as AssetType; if (savedObjectTypes.includes(assetType)) { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index c7f2df38fe41a4..c701762e50b506 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -22,6 +22,7 @@ import { fetchUrl, getResponse, getResponseStream } from './requests'; import { streamToBuffer } from './streams'; import { getRegistryUrl } from './registry_url'; import { appContextService } from '../..'; +import { PackageNotFoundError } from '../../../errors'; export { ArchiveEntry } from './extract'; @@ -76,7 +77,7 @@ export async function fetchFindLatestPackage(packageName: string): Promise { + loadTestFile(require.resolve('./list')); + loadTestFile(require.resolve('./file')); + //loadTestFile(require.resolve('./template')); + loadTestFile(require.resolve('./ilm')); + loadTestFile(require.resolve('./install_overrides')); + loadTestFile(require.resolve('./install_remove_assets')); + loadTestFile(require.resolve('./install_errors')); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts new file mode 100644 index 00000000000000..8acb11b00b57d3 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const kibanaServer = getService('kibanaServer'); + const supertest = getService('supertest'); + + describe('package error handling', async () => { + skipIfNoDockerRegistry(providerContext); + it('should return 404 if package does not exist', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/nonexistent-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .expect(404); + let res; + try { + res = await kibanaServer.savedObjects.get({ + type: 'epm-package', + id: 'nonexistent', + }); + } catch (err) { + res = err; + } + expect(res.response.data.statusCode).equal(404); + }); + it('should return 400 if trying to update/install to an out-of-date package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/update-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + let res; + try { + res = await kibanaServer.savedObjects.get({ + type: 'epm-package', + id: 'update', + }); + } catch (err) { + res = err; + } + expect(res.response.data.statusCode).equal(404); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts index 9ca8ebf136078b..35058de0684b21 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts @@ -108,6 +108,54 @@ export default function (providerContext: FtrProviderContext) { }); expect(resSearch.id).equal('sample_search'); }); + it('should have created the correct saved object', async function () { + const res = await kibanaServer.savedObjects.get({ + type: 'epm-packages', + id: 'all_assets', + }); + expect(res.attributes).eql({ + installed_kibana: [ + { + id: 'sample_dashboard', + type: 'dashboard', + }, + { + id: 'sample_dashboard2', + type: 'dashboard', + }, + { + id: 'sample_search', + type: 'search', + }, + { + id: 'sample_visualization', + type: 'visualization', + }, + ], + installed_es: [ + { + id: 'logs-all_assets.test_logs-0.1.0', + type: 'ingest_pipeline', + }, + { + id: 'logs-all_assets.test_logs', + type: 'index_template', + }, + { + id: 'metrics-all_assets.test_metrics', + type: 'index_template', + }, + ], + es_index_patterns: { + test_logs: 'logs-all_assets.test_logs-*', + test_metrics: 'metrics-all_assets.test_metrics-*', + }, + name: 'all_assets', + version: '0.1.0', + internal: false, + removable: true, + }); + }); }); describe('uninstalls all assets when uninstalling a package', async () => { @@ -192,6 +240,18 @@ export default function (providerContext: FtrProviderContext) { } expect(resSearch.response.data.statusCode).equal(404); }); + it('should have removed the saved object', async function () { + let res; + try { + res = await kibanaServer.savedObjects.get({ + type: 'epm-packages', + id: 'all_assets', + }); + } catch (err) { + res = err; + } + expect(res.response.data.statusCode).equal(404); + }); }); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts index 98b26c1c04ebb7..20414fcb90521b 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts @@ -29,7 +29,7 @@ export default function ({ getService }: FtrProviderContext) { return response.body; }; const listResponse = await fetchPackageList(); - expect(listResponse.response.length).to.be(13); + expect(listResponse.response.length).to.be(14); } else { warnAndSkipTest(this, log); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/fields/fields.yml new file mode 100644 index 00000000000000..12a9a03c1337b4 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/fields/fields.yml @@ -0,0 +1,16 @@ +- name: dataset.type + type: constant_keyword + description: > + Dataset type. +- name: dataset.name + type: constant_keyword + description: > + Dataset name. +- name: dataset.namespace + type: constant_keyword + description: > + Dataset namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/manifest.yml new file mode 100644 index 00000000000000..9ac3c68a0be9ec --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/docs/README.md new file mode 100644 index 00000000000000..13ef3f4fa91526 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing installing or updating to an out-of-date package \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml new file mode 100644 index 00000000000000..b12f1bfbd3b7ed --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: update +title: Package update test +description: This is a test package for updating a package +version: 0.1.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/fields/fields.yml new file mode 100644 index 00000000000000..12a9a03c1337b4 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/fields/fields.yml @@ -0,0 +1,16 @@ +- name: dataset.type + type: constant_keyword + description: > + Dataset type. +- name: dataset.name + type: constant_keyword + description: > + Dataset name. +- name: dataset.namespace + type: constant_keyword + description: > + Dataset namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/manifest.yml new file mode 100644 index 00000000000000..9ac3c68a0be9ec --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/docs/README.md new file mode 100644 index 00000000000000..8e26522d868392 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing installing or updating to an out-of-date package diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml new file mode 100644 index 00000000000000..11dbdc102dce83 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: update +title: Package update test +description: This is a test package for updating a package +version: 0.2.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/ingest_manager_api_integration/apis/index.js b/x-pack/test/ingest_manager_api_integration/apis/index.js index d21b80bd6eed78..72121b2164bfd8 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/index.js @@ -12,12 +12,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./fleet/index')); // EPM - loadTestFile(require.resolve('./epm/list')); - loadTestFile(require.resolve('./epm/file')); - //loadTestFile(require.resolve('./epm/template')); - loadTestFile(require.resolve('./epm/ilm')); - loadTestFile(require.resolve('./epm/install_overrides')); - loadTestFile(require.resolve('./epm/install_remove_assets')); + loadTestFile(require.resolve('./epm/index')); // Package configs loadTestFile(require.resolve('./package_config/create')); From 909c9826ff87e522e29626403103f854089f3d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez?= Date: Mon, 3 Aug 2020 18:27:28 +0200 Subject: [PATCH 14/74] [Metrics UI] Fix typo on view selector in metrics explorer (#74084) --- .../infra/public/components/saved_views/toolbar_control.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx index 2e06ee55189d9e..83fe233553351e 100644 --- a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx @@ -176,7 +176,7 @@ export function SavedViewsToolbarControls(props: Props) { {currentView ? currentView.name : i18n.translate('xpack.infra.savedView.unknownView', { - defaultMessage: 'No view seleted', + defaultMessage: 'No view selected', })} From 0739a3453dcc27fdd422916f098bc263a0a84c4a Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Mon, 3 Aug 2020 18:29:08 +0200 Subject: [PATCH 15/74] [SIEM] Adds threshold rule creation Cypress test (#74065) * adds threshold rule creation cypress test * fixes type chek error Co-authored-by: Elastic Machine --- .../alerts_detection_rules_threshold.spec.ts | 174 ++++++++++++++++++ .../security_solution/cypress/objects/rule.ts | 21 +++ .../cypress/screens/create_new_rule.ts | 8 + .../cypress/screens/rule_details.ts | 2 + .../cypress/tasks/create_new_rule.ts | 40 +++- 5 files changed, 238 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts new file mode 100644 index 00000000000000..10f9ebb5623df5 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { newThresholdRule } from '../objects/rule'; + +import { + CUSTOM_RULES_BTN, + RISK_SCORE, + RULE_NAME, + RULES_ROW, + RULES_TABLE, + SEVERITY, +} from '../screens/alerts_detection_rules'; +import { + ABOUT_FALSE_POSITIVES, + ABOUT_INVESTIGATION_NOTES, + ABOUT_MITRE, + ABOUT_RISK, + ABOUT_RULE_DESCRIPTION, + ABOUT_SEVERITY, + ABOUT_STEP, + ABOUT_TAGS, + ABOUT_URLS, + DEFINITION_CUSTOM_QUERY, + DEFINITION_INDEX_PATTERNS, + DEFINITION_THRESHOLD, + DEFINITION_TIMELINE, + DEFINITION_STEP, + INVESTIGATION_NOTES_MARKDOWN, + INVESTIGATION_NOTES_TOGGLE, + RULE_ABOUT_DETAILS_HEADER_TOGGLE, + RULE_NAME_HEADER, + SCHEDULE_LOOPBACK, + SCHEDULE_RUNS, + SCHEDULE_STEP, +} from '../screens/rule_details'; + +import { + goToManageAlertsDetectionRules, + waitForAlertsIndexToBeCreated, + waitForAlertsPanelToBeLoaded, +} from '../tasks/alerts'; +import { + changeToThreeHundredRowsPerPage, + filterByCustomRules, + goToCreateNewRule, + goToRuleDetails, + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, + waitForRulesToBeLoaded, +} from '../tasks/alerts_detection_rules'; +import { + createAndActivateRule, + fillAboutRuleAndContinue, + fillDefineThresholdRuleAndContinue, + selectThresholdRuleType, +} from '../tasks/create_new_rule'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; + +import { DETECTIONS_URL } from '../urls/navigation'; + +describe('Detection rules, threshold', () => { + before(() => { + esArchiverLoad('timeline'); + }); + + after(() => { + esArchiverUnload('timeline'); + }); + + it('Creates and activates a new threshold rule', () => { + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertsDetectionRules(); + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + goToCreateNewRule(); + selectThresholdRuleType(); + fillDefineThresholdRuleAndContinue(newThresholdRule); + fillAboutRuleAndContinue(newThresholdRule); + createAndActivateRule(); + + cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)'); + + changeToThreeHundredRowsPerPage(); + waitForRulesToBeLoaded(); + + const expectedNumberOfRules = 1; + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); + }); + + filterByCustomRules(); + + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', 1); + }); + cy.get(RULE_NAME).invoke('text').should('eql', newThresholdRule.name); + cy.get(RISK_SCORE).invoke('text').should('eql', newThresholdRule.riskScore); + cy.get(SEVERITY).invoke('text').should('eql', newThresholdRule.severity); + cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true'); + + goToRuleDetails(); + + let expectedUrls = ''; + newThresholdRule.referenceUrls.forEach((url) => { + expectedUrls = expectedUrls + url; + }); + let expectedFalsePositives = ''; + newThresholdRule.falsePositivesExamples.forEach((falsePositive) => { + expectedFalsePositives = expectedFalsePositives + falsePositive; + }); + let expectedTags = ''; + newThresholdRule.tags.forEach((tag) => { + expectedTags = expectedTags + tag; + }); + let expectedMitre = ''; + newThresholdRule.mitre.forEach((mitre) => { + expectedMitre = expectedMitre + mitre.tactic; + mitre.techniques.forEach((technique) => { + expectedMitre = expectedMitre + technique; + }); + }); + const expectedIndexPatterns = [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ]; + + cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${newThresholdRule.name} Beta`); + + cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', newThresholdRule.description); + cy.get(ABOUT_STEP).eq(ABOUT_SEVERITY).invoke('text').should('eql', newThresholdRule.severity); + cy.get(ABOUT_STEP).eq(ABOUT_RISK).invoke('text').should('eql', newThresholdRule.riskScore); + cy.get(ABOUT_STEP).eq(ABOUT_URLS).invoke('text').should('eql', expectedUrls); + cy.get(ABOUT_STEP) + .eq(ABOUT_FALSE_POSITIVES) + .invoke('text') + .should('eql', expectedFalsePositives); + cy.get(ABOUT_STEP).eq(ABOUT_MITRE).invoke('text').should('eql', expectedMitre); + cy.get(ABOUT_STEP).eq(ABOUT_TAGS).invoke('text').should('eql', expectedTags); + + cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); + cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', INVESTIGATION_NOTES_MARKDOWN); + + cy.get(DEFINITION_INDEX_PATTERNS).then((patterns) => { + cy.wrap(patterns).each((pattern, index) => { + cy.wrap(pattern).invoke('text').should('eql', expectedIndexPatterns[index]); + }); + }); + cy.get(DEFINITION_STEP) + .eq(DEFINITION_CUSTOM_QUERY) + .invoke('text') + .should('eql', `${newThresholdRule.customQuery} `); + cy.get(DEFINITION_STEP).eq(DEFINITION_TIMELINE).invoke('text').should('eql', 'None'); + cy.get(DEFINITION_STEP) + .eq(DEFINITION_THRESHOLD) + .invoke('text') + .should( + 'eql', + `Results aggregated by ${newThresholdRule.thresholdField} >= ${newThresholdRule.threshold}` + ); + + cy.get(SCHEDULE_STEP).eq(SCHEDULE_RUNS).invoke('text').should('eql', '5m'); + cy.get(SCHEDULE_STEP).eq(SCHEDULE_LOOPBACK).invoke('text').should('eql', '1m'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index a30fddc3c3a69e..aeadc34c6e49c6 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -33,6 +33,11 @@ export interface CustomRule { timelineId: string; } +export interface ThresholdRule extends CustomRule { + thresholdField: string; + threshold: string; +} + export interface MachineLearningRule { machineLearningJob: string; anomalyScoreThreshold: string; @@ -72,6 +77,22 @@ export const newRule: CustomRule = { timelineId: '0162c130-78be-11ea-9718-118a926974a4', }; +export const newThresholdRule: ThresholdRule = { + customQuery: 'host.name:*', + name: 'New Rule Test', + description: 'The new rule description.', + severity: 'High', + riskScore: '17', + tags: ['test', 'newRule'], + referenceUrls: ['https://www.google.com/', 'https://elastic.co/'], + falsePositivesExamples: ['False1', 'False2'], + mitre: [mitre1, mitre2], + note: '# test markdown', + timelineId: '0162c130-78be-11ea-9718-118a926974a4', + thresholdField: 'host.name', + threshold: '10', +}; + export const machineLearningRule: MachineLearningRule = { machineLearningJob: 'linux_anomalous_network_service', anomalyScoreThreshold: '20', diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index bc0740554bc522..af4fe7257ae5b5 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -27,6 +27,8 @@ export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]'; export const IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK = '[data-test-subj="importQueryFromSavedTimeline"]'; +export const INPUT = '[data-test-subj="input"]'; + export const INVESTIGATION_NOTES_TEXTAREA = '[data-test-subj="detectionEngineStepAboutRuleNote"] textarea'; @@ -64,3 +66,9 @@ export const SEVERITY_DROPDOWN = export const TAGS_INPUT = '[data-test-subj="detectionEngineStepAboutRuleTags"] [data-test-subj="comboBoxSearchInput"]'; + +export const THRESHOLD_FIELD_SELECTION = '.euiFilterSelectItem'; + +export const THRESHOLD_INPUT_AREA = '[data-test-subj="thresholdInput"]'; + +export const THRESHOLD_TYPE = '[data-test-subj="thresholdRuleType"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index ec57e142125da7..1c0102382ab6b8 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -26,6 +26,8 @@ export const ANOMALY_SCORE = 1; export const DEFINITION_CUSTOM_QUERY = 1; +export const DEFINITION_THRESHOLD = 4; + export const DEFINITION_TIMELINE = 3; export const DEFINITION_INDEX_PATTERNS = diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 88ae582b58891c..de9d343bc91f7f 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -3,7 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CustomRule, MachineLearningRule, machineLearningRule } from '../objects/rule'; + +import { + CustomRule, + MachineLearningRule, + machineLearningRule, + ThresholdRule, +} from '../objects/rule'; import { ABOUT_CONTINUE_BTN, ANOMALY_THRESHOLD_INPUT, @@ -15,6 +21,7 @@ import { DEFINE_CONTINUE_BUTTON, FALSE_POSITIVES_INPUT, IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK, + INPUT, INVESTIGATION_NOTES_TEXTAREA, MACHINE_LEARNING_DROPDOWN, MACHINE_LEARNING_LIST, @@ -30,6 +37,9 @@ import { SCHEDULE_CONTINUE_BUTTON, SEVERITY_DROPDOWN, TAGS_INPUT, + THRESHOLD_FIELD_SELECTION, + THRESHOLD_INPUT_AREA, + THRESHOLD_TYPE, } from '../screens/create_new_rule'; import { TIMELINE } from '../screens/timeline'; @@ -39,7 +49,9 @@ export const createAndActivateRule = () => { cy.get(CREATE_AND_ACTIVATE_BTN).should('not.exist'); }; -export const fillAboutRuleAndContinue = (rule: CustomRule | MachineLearningRule) => { +export const fillAboutRuleAndContinue = ( + rule: CustomRule | MachineLearningRule | ThresholdRule +) => { cy.get(RULE_NAME_INPUT).type(rule.name, { force: true }); cy.get(RULE_DESCRIPTION_INPUT).type(rule.description, { force: true }); @@ -80,18 +92,28 @@ export const fillAboutRuleAndContinue = (rule: CustomRule | MachineLearningRule) cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true }); }; -export const fillDefineCustomRuleAndContinue = (rule: CustomRule) => { - cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery); +export const fillDefineCustomRuleWithImportedQueryAndContinue = (rule: CustomRule) => { + cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); + cy.get(TIMELINE(rule.timelineId)).click(); cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); }; -export const fillDefineCustomRuleWithImportedQueryAndContinue = (rule: CustomRule) => { - cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); - cy.get(TIMELINE(rule.timelineId)).click(); +export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { + const thresholdField = 0; + const threshold = 1; + + cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery); cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery); + cy.get(THRESHOLD_INPUT_AREA) + .find(INPUT) + .then((inputs) => { + cy.wrap(inputs[thresholdField]).type(rule.thresholdField); + cy.get(THRESHOLD_FIELD_SELECTION).click({ force: true }); + cy.wrap(inputs[threshold]).clear().type(rule.threshold); + }); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); @@ -111,3 +133,7 @@ export const fillDefineMachineLearningRuleAndContinue = (rule: MachineLearningRu export const selectMachineLearningRuleType = () => { cy.get(MACHINE_LEARNING_TYPE).click({ force: true }); }; + +export const selectThresholdRuleType = () => { + cy.get(THRESHOLD_TYPE).click({ force: true }); +}; From df41c9b274ee6e472742842f5704f256d9d9ee38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Mon, 3 Aug 2020 18:05:39 +0100 Subject: [PATCH 16/74] [Telemetry] Data: Report dataset info only if there is known metadata (#71419) * [Telemetry] Data: Report dataset information only if there is known metadata * Handle data-stream indices (.ds-*) Co-authored-by: Elastic Machine --- .../get_data_telemetry.test.ts | 55 ++++++++++++- .../get_data_telemetry/get_data_telemetry.ts | 77 +++++++++++++------ 2 files changed, 108 insertions(+), 24 deletions(-) diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts index 8bffc5d012a741..ad19def160200b 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts @@ -75,7 +75,7 @@ describe('get_data_telemetry', () => { { name: 'logs-endpoint.1234', docCount: 0 }, // Matching pattern with a dot in the name // New Indexing strategy: everything can be inferred from the constant_keyword values { - name: 'logs-nginx.access-default-000001', + name: '.ds-logs-nginx.access-default-000001', datasetName: 'nginx.access', datasetType: 'logs', shipper: 'filebeat', @@ -84,7 +84,7 @@ describe('get_data_telemetry', () => { sizeInBytes: 1000, }, { - name: 'logs-nginx.access-default-000002', + name: '.ds-logs-nginx.access-default-000002', datasetName: 'nginx.access', datasetType: 'logs', shipper: 'filebeat', @@ -92,6 +92,42 @@ describe('get_data_telemetry', () => { docCount: 1000, sizeInBytes: 60, }, + { + name: '.ds-traces-something-default-000002', + datasetName: 'something', + datasetType: 'traces', + packageName: 'some-package', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, + { + name: '.ds-metrics-something.else-default-000002', + datasetName: 'something.else', + datasetType: 'metrics', + managedBy: 'ingest-manager', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, + // Filter out if it has datasetName and datasetType but none of the shipper, packageName or managedBy === 'ingest-manager' + { + name: 'some-index-that-should-not-show', + datasetName: 'should-not-show', + datasetType: 'logs', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, + { + name: 'other-index-that-should-not-show', + datasetName: 'should-not-show-either', + datasetType: 'metrics', + managedBy: 'me', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, ]) ).toStrictEqual([ { @@ -138,6 +174,21 @@ describe('get_data_telemetry', () => { doc_count: 2000, size_in_bytes: 1060, }, + { + dataset: { name: 'something', type: 'traces' }, + package: { name: 'some-package' }, + index_count: 1, + ecs_index_count: 1, + doc_count: 1000, + size_in_bytes: 60, + }, + { + dataset: { name: 'something.else', type: 'metrics' }, + index_count: 1, + ecs_index_count: 1, + doc_count: 1000, + size_in_bytes: 60, + }, ]); }); }); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts index cf906bc5c86cfc..079f510bb256a1 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts @@ -36,6 +36,9 @@ export interface DataTelemetryDocument extends DataTelemetryBasePayload { name?: string; type?: DataTelemetryType | 'unknown' | string; // The union of types is to help autocompletion with some known `dataset.type`s }; + package?: { + name: string; + }; shipper?: string; pattern_name?: DataPatternName; } @@ -44,6 +47,8 @@ export type DataTelemetryPayload = DataTelemetryDocument[]; export interface DataTelemetryIndex { name: string; + packageName?: string; // Populated by Ingest Manager at `_meta.package.name` + managedBy?: string; // Populated by Ingest Manager at `_meta.managed_by` datasetName?: string; // To be obtained from `mappings.dataset.name` if it's a constant keyword datasetType?: string; // To be obtained from `mappings.dataset.type` if it's a constant keyword shipper?: string; // To be obtained from `_meta.beat` if it's set @@ -58,6 +63,7 @@ export interface DataTelemetryIndex { type AtLeastOne }> = Partial & U[keyof U]; type DataDescriptor = AtLeastOne<{ + packageName: string; datasetName: string; datasetType: string; shipper: string; @@ -67,17 +73,28 @@ type DataDescriptor = AtLeastOne<{ function findMatchingDescriptors({ name, shipper, + packageName, + managedBy, datasetName, datasetType, }: DataTelemetryIndex): DataDescriptor[] { // If we already have the data from the indices' mappings... - if ([shipper, datasetName, datasetType].some(Boolean)) { + if ( + [shipper, packageName].some(Boolean) || + (managedBy === 'ingest-manager' && [datasetType, datasetName].some(Boolean)) + ) { return [ { ...(shipper && { shipper }), + ...(packageName && { packageName }), ...(datasetName && { datasetName }), ...(datasetType && { datasetType }), - } as AtLeastOne<{ datasetName: string; datasetType: string; shipper: string }>, // Using casting here because TS doesn't infer at least one exists from the if clause + } as AtLeastOne<{ + packageName: string; + datasetName: string; + datasetType: string; + shipper: string; + }>, // Using casting here because TS doesn't infer at least one exists from the if clause ]; } @@ -122,6 +139,7 @@ export function buildDataTelemetryPayload(indices: DataTelemetryIndex[]): DataTe ({ name }) => !( name.startsWith('.') && + !name.startsWith('.ds-') && // data_stream-related indices can be included !startingDotPatternsUntilTheFirstAsterisk.find((pattern) => name.startsWith(pattern)) ) ); @@ -130,10 +148,17 @@ export function buildDataTelemetryPayload(indices: DataTelemetryIndex[]): DataTe for (const indexCandidate of indexCandidates) { const matchingDescriptors = findMatchingDescriptors(indexCandidate); - for (const { datasetName, datasetType, shipper, patternName } of matchingDescriptors) { - const key = `${datasetName}-${datasetType}-${shipper}-${patternName}`; + for (const { + datasetName, + datasetType, + packageName, + shipper, + patternName, + } of matchingDescriptors) { + const key = `${datasetName}-${datasetType}-${packageName}-${shipper}-${patternName}`; acc.set(key, { ...((datasetName || datasetType) && { dataset: { name: datasetName, type: datasetType } }), + ...(packageName && { package: { name: packageName } }), ...(shipper && { shipper }), ...(patternName && { pattern_name: patternName }), ...increaseCounters(acc.get(key), indexCandidate), @@ -165,6 +190,12 @@ interface IndexMappings { mappings: { _meta?: { beat?: string; + + // Ingest Manager provided metadata + package?: { + name?: string; + }; + managed_by?: string; // Typically "ingest-manager" }; properties: { dataset?: { @@ -195,7 +226,7 @@ export async function getDataTelemetry(callCluster: LegacyAPICaller) { try { const index = [ ...DATA_DATASETS_INDEX_PATTERNS_UNIQUE.map(({ pattern }) => pattern), - '*-*-*-*', // Include new indexing strategy indices {type}-{dataset}-{namespace}-{rollover_counter} + '*-*-*', // Include data-streams aliases `{type}-{dataset}-{namespace}` ]; const [indexMappings, indexStats]: [IndexMappings, IndexStats] = await Promise.all([ // GET */_mapping?filter_path=*.mappings._meta.beat,*.mappings.properties.ecs.properties.version.type,*.mappings.properties.dataset.properties.type.value,*.mappings.properties.dataset.properties.name.value @@ -204,16 +235,17 @@ export async function getDataTelemetry(callCluster: LegacyAPICaller) { filterPath: [ // _meta.beat tells the shipper '*.mappings._meta.beat', + // _meta.package.name tells the Ingest Manager's package + '*.mappings._meta.package.name', + // _meta.managed_by is usually populated by Ingest Manager for the UI to identify it + '*.mappings._meta.managed_by', // Does it have `ecs.version` in the mappings? => It follows the ECS conventions '*.mappings.properties.ecs.properties.version.type', - // Disable the fields below because they are still pending to be confirmed: - // https://github.com/elastic/ecs/pull/845 - // TODO: Re-enable when the final fields are confirmed - // // If `dataset.type` is a `constant_keyword`, it can be reported as a type - // '*.mappings.properties.dataset.properties.type.value', - // // If `dataset.name` is a `constant_keyword`, it can be reported as the dataset - // '*.mappings.properties.dataset.properties.name.value', + // If `dataset.type` is a `constant_keyword`, it can be reported as a type + '*.mappings.properties.dataset.properties.type.value', + // If `dataset.name` is a `constant_keyword`, it can be reported as the dataset + '*.mappings.properties.dataset.properties.name.value', ], }), // GET /_stats/docs,store?level=indices&filter_path=indices.*.total @@ -227,24 +259,25 @@ export async function getDataTelemetry(callCluster: LegacyAPICaller) { const indexNames = Object.keys({ ...indexMappings, ...indexStats?.indices }); const indices = indexNames.map((name) => { - const isECS = !!indexMappings[name]?.mappings?.properties.ecs?.properties.version?.type; - const shipper = indexMappings[name]?.mappings?._meta?.beat; - const datasetName = indexMappings[name]?.mappings?.properties.dataset?.properties.name?.value; - const datasetType = indexMappings[name]?.mappings?.properties.dataset?.properties.type?.value; + const baseIndexInfo = { + name, + isECS: !!indexMappings[name]?.mappings?.properties.ecs?.properties.version?.type, + shipper: indexMappings[name]?.mappings?._meta?.beat, + packageName: indexMappings[name]?.mappings?._meta?.package?.name, + managedBy: indexMappings[name]?.mappings?._meta?.managed_by, + datasetName: indexMappings[name]?.mappings?.properties.dataset?.properties.name?.value, + datasetType: indexMappings[name]?.mappings?.properties.dataset?.properties.type?.value, + }; const stats = (indexStats?.indices || {})[name]; if (stats) { return { - name, - datasetName, - datasetType, - shipper, - isECS, + ...baseIndexInfo, docCount: stats.total?.docs?.count, sizeInBytes: stats.total?.store?.size_in_bytes, }; } - return { name, datasetName, datasetType, shipper, isECS }; + return baseIndexInfo; }); return buildDataTelemetryPayload(indices); } catch (e) { From e82f14af75df99417921f7092702c0edf895eeea Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 3 Aug 2020 13:07:39 -0400 Subject: [PATCH 17/74] fix bug when clicking on start a new case (#74092) --- .../recent_cases/no_cases/index.test.tsx | 49 +++++++++++++++++++ .../recent_cases/no_cases/index.tsx | 3 +- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx new file mode 100644 index 00000000000000..99902a31975d0d --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { useKibana } from '../../../../common/lib/kibana'; +import '../../../../common/mock/match_media'; +import { createUseKibanaMock, TestProviders } from '../../../../common/mock'; +import { NoCases } from '.'; + +jest.mock('../../../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.Mock; + +let navigateToApp: jest.Mock; + +describe('RecentCases', () => { + beforeEach(() => { + jest.resetAllMocks(); + navigateToApp = jest.fn(); + const kibanaMock = createUseKibanaMock()(); + useKibanaMock.mockReturnValue({ + ...kibanaMock, + services: { + application: { + navigateToApp, + getUrlForApp: jest.fn(), + }, + }, + }); + }); + + it('if no cases, you should be able to create a case by clicking on the link "start a new case"', () => { + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="no-cases-create-case"]`).first().simulate('click'); + expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { + path: + "/create?timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx index 40969a6e1df4a6..875a678f322266 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx @@ -21,7 +21,7 @@ const NoCasesComponent = () => { const goToCreateCase = useCallback( (ev) => { ev.preventDefault(); - navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { + navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { path: getCreateCaseUrl(search), }); }, @@ -30,6 +30,7 @@ const NoCasesComponent = () => { const newCaseLink = useMemo( () => ( {` ${i18n.START_A_NEW_CASE}`} From 6337cbf16397881cc39f60ad37a0d79d431b610e Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Mon, 3 Aug 2020 19:34:01 +0200 Subject: [PATCH 18/74] updates exceptions 'Add to clipboard' copy for comments (#74103) Co-authored-by: Elastic Machine --- .../public/common/components/exceptions/translations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts index b826c1e49f2749..e68b903266428f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts @@ -176,7 +176,7 @@ export const ADD_COMMENT_PLACEHOLDER = i18n.translate( export const ADD_TO_CLIPBOARD = i18n.translate( 'xpack.securitySolution.exceptions.viewer.addToClipboard', { - defaultMessage: 'Add to clipboard', + defaultMessage: 'Comment', } ); From 89f1ac49870d06624eca635b82c83bce4f0545df Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Mon, 3 Aug 2020 19:07:03 +0100 Subject: [PATCH 19/74] fix default timeline's tab (#74051) --- .../components/open_timeline/index.test.tsx | 172 ++++++++++++++++-- .../open_timeline/use_timeline_types.tsx | 7 +- 2 files changed, 161 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index 75b6413bf08f9b..43fd57fcfc5bfa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx @@ -4,29 +4,48 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + +import React from 'react'; +import { renderHook, act } from '@testing-library/react-hooks'; import { mount } from 'enzyme'; import { MockedProvider } from 'react-apollo/test-utils'; -import React from 'react'; - // we don't have the types for waitFor just yet, so using "as waitFor" until when we do import { wait as waitFor } from '@testing-library/react'; +import { useHistory, useParams } from 'react-router-dom'; + import '../../../common/mock/match_media'; +import { SecurityPageName } from '../../../app/types'; +import { TimelineType } from '../../../../common/types/timeline'; + import { TestProviders, apolloClient } from '../../../common/mock/test_providers'; import { mockOpenTimelineQueryResults } from '../../../common/mock/timeline_results'; +import { getTimelineTabsUrl } from '../../../common/components/link_to'; + import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; +import { useGetAllTimeline, getAllTimeline } from '../../containers/all'; import { NotePreviews } from './note_previews'; import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; import { StatefulOpenTimeline } from '.'; -import { useGetAllTimeline, getAllTimeline } from '../../containers/all'; +import { TimelineTabsStyle } from './types'; +import { + useTimelineTypes, + UseTimelineTypesArgs, + UseTimelineTypesResult, +} from './use_timeline_types'; -import { useParams } from 'react-router-dom'; -import { TimelineType } from '../../../../common/types/timeline'; +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); -jest.mock('../../../common/lib/kibana'); -jest.mock('../../../common/components/link_to'); + return { + ...originalModule, + useParams: jest.fn(), + useHistory: jest.fn(), + }; +}); jest.mock('./helpers', () => { const originalModule = jest.requireActual('./helpers'); @@ -41,24 +60,31 @@ jest.mock('../../containers/all', () => { return { ...originalModule, useGetAllTimeline: jest.fn(), - getAllTimeline: originalModule.getAllTimeline, }; }); -jest.mock('react-router-dom', () => { - const originalModule = jest.requireActual('react-router-dom'); +jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/components/link_to'); +jest.mock('../../../common/components/link_to', () => { + const originalModule = jest.requireActual('../../../common/components/link_to'); return { ...originalModule, - useParams: jest.fn(), - useHistory: jest.fn().mockReturnValue([]), + getTimelineTabsUrl: jest.fn(), + useFormatUrl: jest.fn().mockReturnValue({ formatUrl: jest.fn(), search: 'urlSearch' }), }; }); describe('StatefulOpenTimeline', () => { const title = 'All Timelines / Open Timelines'; + let mockHistory: History[]; beforeEach(() => { - (useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.default }); + (useParams as jest.Mock).mockReturnValue({ + tabName: TimelineType.default, + pageName: SecurityPageName.timelines, + }); + mockHistory = []; + (useHistory as jest.Mock).mockReturnValue(mockHistory); ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ fetchAllTimeline: jest.fn(), timelines: getAllTimeline( @@ -71,6 +97,13 @@ describe('StatefulOpenTimeline', () => { }); }); + afterEach(() => { + (getTimelineTabsUrl as jest.Mock).mockClear(); + (useParams as jest.Mock).mockClear(); + (useHistory as jest.Mock).mockClear(); + mockHistory = []; + }); + test('it has the expected initial state', () => { const wrapper = mount( @@ -101,6 +134,109 @@ describe('StatefulOpenTimeline', () => { }); }); + describe("Template timelines' tab", () => { + test("should land on correct timelines' tab with url timelines/default", () => { + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 0 }), + { + wrapper: ({ children }) => {children}, + } + ); + + expect(result.current.timelineType).toBe(TimelineType.default); + }); + + test("should land on correct timelines' tab with url timelines/template", () => { + (useParams as jest.Mock).mockReturnValue({ + tabName: TimelineType.template, + pageName: SecurityPageName.timelines, + }); + + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 0 }), + { + wrapper: ({ children }) => {children}, + } + ); + + expect(result.current.timelineType).toBe(TimelineType.template); + }); + + test("should land on correct templates' tab after switching tab", () => { + (useParams as jest.Mock).mockReturnValue({ + tabName: TimelineType.template, + pageName: SecurityPageName.timelines, + }); + + const wrapper = mount( + + + + + + ); + wrapper + .find(`[data-test-subj="timeline-${TimelineTabsStyle.tab}-${TimelineType.template}"]`) + .first() + .simulate('click'); + act(() => { + expect(history.length).toBeGreaterThan(0); + }); + }); + + test("should selecting correct timelines' filter", () => { + (useParams as jest.Mock).mockReturnValue({ + tabName: 'mockTabName', + pageName: SecurityPageName.case, + }); + + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 0 }), + { + wrapper: ({ children }) => {children}, + } + ); + + expect(result.current.timelineType).toBe(TimelineType.default); + }); + + test('should not change url after switching filter', () => { + (useParams as jest.Mock).mockReturnValue({ + tabName: 'mockTabName', + pageName: SecurityPageName.case, + }); + + const wrapper = mount( + + + + + + ); + wrapper + .find( + `[data-test-subj="open-timeline-modal-body-${TimelineTabsStyle.filter}-${TimelineType.template}"]` + ) + .first() + .simulate('click'); + act(() => { + expect(mockHistory.length).toEqual(0); + }); + }); + }); + describe('#onQueryChange', () => { test('it updates the query state with the expected trimmed value when the user enters a query', () => { const wrapper = mount( @@ -482,9 +618,13 @@ describe('StatefulOpenTimeline', () => { ); - expect(wrapper.find('[data-test-subj="open-timeline-modal-body-filters"]').exists()).toEqual( - true - ); + expect( + wrapper + .find( + `[data-test-subj="open-timeline-modal-body-${TimelineTabsStyle.filter}-${TimelineType.default}"]` + ) + .exists() + ).toEqual(true); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx index 55afe845cdfb3b..1ffa626b013114 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx @@ -33,7 +33,9 @@ export const useTimelineTypes = ({ const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.timelines); const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>(); const [timelineType, setTimelineTypes] = useState( - tabName === TimelineType.default || tabName === TimelineType.template ? tabName : null + tabName === TimelineType.default || tabName === TimelineType.template + ? tabName + : TimelineType.default ); const goToTimeline = useCallback( @@ -114,6 +116,7 @@ export const useTimelineTypes = ({ {getFilterOrTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => ( { return getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => ( Date: Mon, 3 Aug 2020 21:41:34 +0300 Subject: [PATCH 20/74] [Security Solutions][Case] Fix quote handling (#74093) --- .../components/add_comment/index.test.tsx | 10 +- .../cases/components/add_comment/index.tsx | 150 ++++++++++-------- .../components/user_action_tree/index.tsx | 21 +-- 3 files changed, 98 insertions(+), 83 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx index 88969c3ae5fb3d..f697ce443f2c56 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { AddComment } from '.'; +import { AddComment, AddCommentRefObject } from '.'; import { TestProviders } from '../../../common/mock'; import { getFormMock } from '../__mock__/form'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; @@ -60,9 +60,11 @@ const defaultPostCommment = { isError: false, postComment, }; + const sampleData = { comment: 'what a cool comment', }; + describe('AddComment ', () => { const formHookMock = getFormMock(sampleData); @@ -122,16 +124,18 @@ describe('AddComment ', () => { ).toBeTruthy(); }); - it('should insert a quote if one is available', () => { + it('should insert a quote', () => { const sampleQuote = 'what a cool quote'; + const ref = React.createRef(); mount( - + ); + ref.current!.addQuote(sampleQuote); expect(formHookMock.setFieldValue).toBeCalledWith( 'comment', `${sampleData.comment}\n\n${sampleQuote}` diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index a54cf142c18b71..87bd7bb247056a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, forwardRef, useImperativeHandle } from 'react'; import styled from 'styled-components'; import { CommentRequest } from '../../../../../case/common/api'; @@ -30,88 +30,98 @@ const initialCommentValue: CommentRequest = { comment: '', }; +export interface AddCommentRefObject { + addQuote: (quote: string) => void; +} + interface AddCommentProps { caseId: string; disabled?: boolean; - insertQuote: string | null; onCommentSaving?: () => void; onCommentPosted: (newCase: Case) => void; showLoading?: boolean; } -export const AddComment = React.memo( - ({ caseId, disabled, insertQuote, showLoading = true, onCommentPosted, onCommentSaving }) => { - const { isLoading, postComment } = usePostComment(caseId); - const { form } = useForm({ - defaultValue: initialCommentValue, - options: { stripEmptyFields: false }, - schema, - }); - const { getFormData, setFieldValue, reset, submit } = form; - const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( - form, - 'comment' - ); +export const AddComment = React.memo( + forwardRef( + ({ caseId, disabled, showLoading = true, onCommentPosted, onCommentSaving }, ref) => { + const { isLoading, postComment } = usePostComment(caseId); + const { form } = useForm({ + defaultValue: initialCommentValue, + options: { stripEmptyFields: false }, + schema, + }); + const { getFormData, setFieldValue, reset, submit } = form; + const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( + form, + 'comment' + ); + + const addQuote = useCallback( + (quote) => { + const { comment } = getFormData(); + setFieldValue('comment', `${comment}${comment.length > 0 ? '\n\n' : ''}${quote}`); + }, + [getFormData, setFieldValue] + ); - useEffect(() => { - if (insertQuote !== null) { - const { comment } = getFormData(); - setFieldValue('comment', `${comment}${comment.length > 0 ? '\n\n' : ''}${insertQuote}`); - } - }, [getFormData, insertQuote, setFieldValue]); + useImperativeHandle(ref, () => ({ + addQuote, + })); - const handleTimelineClick = useTimelineClick(); + const handleTimelineClick = useTimelineClick(); - const onSubmit = useCallback(async () => { - const { isValid, data } = await submit(); - if (isValid) { - if (onCommentSaving != null) { - onCommentSaving(); + const onSubmit = useCallback(async () => { + const { isValid, data } = await submit(); + if (isValid) { + if (onCommentSaving != null) { + onCommentSaving(); + } + postComment(data, onCommentPosted); + reset(); } - postComment(data, onCommentPosted); - reset(); - } - }, [onCommentPosted, onCommentSaving, postComment, reset, submit]); + }, [onCommentPosted, onCommentSaving, postComment, reset, submit]); - return ( - - {isLoading && showLoading && } -
- - {i18n.ADD_COMMENT} - - ), - topRightContent: ( - - ), - }} - /> - -
- ); - } + return ( + + {isLoading && showLoading && } +
+ + {i18n.ADD_COMMENT} + + ), + topRightContent: ( + + ), + }} + /> + +
+ ); + } + ) ); AddComment.displayName = 'AddComment'; diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx index 0c1da8694bf1ae..733e3db3c25e60 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx @@ -14,7 +14,7 @@ import * as i18n from '../case_view/translations'; import { Case, CaseUserActions } from '../../containers/types'; import { useUpdateComment } from '../../containers/use_update_comment'; import { useCurrentUser } from '../../../common/lib/kibana'; -import { AddComment } from '../add_comment'; +import { AddComment, AddCommentRefObject } from '../add_comment'; import { getLabelTitle } from './helpers'; import { UserActionItem } from './user_action_item'; import { UserActionMarkdown } from './user_action_markdown'; @@ -58,12 +58,12 @@ export const UserActionTree = React.memo( }: UserActionTreeProps) => { const { commentId } = useParams(); const handlerTimeoutId = useRef(0); + const addCommentRef = useRef(null); const [initLoading, setInitLoading] = useState(true); const [selectedOutlineCommentId, setSelectedOutlineCommentId] = useState(''); const { isLoadingIds, patchComment } = useUpdateComment(); const currentUser = useCurrentUser(); const [manageMarkdownEditIds, setManangeMardownEditIds] = useState([]); - const [insertQuote, setInsertQuote] = useState(null); const handleManageMarkdownEditId = useCallback( (id: string) => { if (!manageMarkdownEditIds.includes(id)) { @@ -111,14 +111,17 @@ export const UserActionTree = React.memo( window.clearTimeout(handlerTimeoutId.current); }, 2400); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [handlerTimeoutId.current] + [handlerTimeoutId] ); const handleManageQuote = useCallback( (quote: string) => { const addCarrots = quote.replace(new RegExp('\r?\n', 'g'), ' \n> '); - setInsertQuote(`> ${addCarrots} \n`); + + if (addCommentRef && addCommentRef.current) { + addCommentRef.current.addQuote(`> ${addCarrots} \n`); + } + handleOutlineComment('add-comment'); }, [handleOutlineComment] @@ -152,14 +155,13 @@ export const UserActionTree = React.memo( ), - // eslint-disable-next-line react-hooks/exhaustive-deps - [caseData.id, handleUpdate, insertQuote, userCanCrud] + [caseData.id, handleUpdate, userCanCrud, handleManageMarkdownEditId] ); useEffect(() => { @@ -169,8 +171,7 @@ export const UserActionTree = React.memo( handleOutlineComment(commentId); } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [commentId, initLoading, isLoadingUserActions, isLoadingIds]); + }, [commentId, initLoading, isLoadingUserActions, isLoadingIds, handleOutlineComment]); return ( <> Date: Mon, 3 Aug 2020 19:42:28 +0100 Subject: [PATCH 21/74] fix legend's color (#74115) --- .../components/charts/barchart.test.tsx | 65 +++++++++++++++++++ .../common/components/charts/barchart.tsx | 2 +- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx index 8617388f4ffb5d..64c8fde87a6bcf 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx @@ -356,6 +356,71 @@ describe.each(chartDataSets)('BarChart with stackByField', () => { }); }); +describe.each(chartDataSets)('BarChart with custom color', () => { + let wrapper: ReactWrapper; + + const data = [ + { + key: 'python.exe', + value: [ + { + x: 1586754900000, + y: 9675, + g: 'python.exe', + }, + ], + color: '#1EA591', + }, + { + key: 'kernel', + value: [ + { + x: 1586754900000, + y: 8708, + g: 'kernel', + }, + { + x: 1586757600000, + y: 9282, + g: 'kernel', + }, + ], + color: '#000000', + }, + { + key: 'sshd', + value: [ + { + x: 1586754900000, + y: 5907, + g: 'sshd', + }, + ], + color: '#ffffff', + }, + ]; + + const expectedColors = ['#1EA591', '#000000', '#ffffff']; + + const stackByField = 'process.name'; + + beforeAll(() => { + wrapper = mount( + + + + + + ); + }); + + expectedColors.forEach((color, i) => { + test(`it renders the expected legend color ${color} for legend item ${i}`, () => { + expect(wrapper.find(`div [color="${color}"]`).exists()).toBe(true); + }); + }); +}); + describe.each(chartHolderDataSets)('BarChart with invalid data [%o]', (data) => { let shallowWrapper: ShallowWrapper; diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx index fba8c3faa9237f..cafb0095431f1d 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx @@ -133,7 +133,7 @@ export const BarChartComponent: React.FC = ({ () => barChart != null && stackByField != null ? barChart.map((d, i) => ({ - color: d.color ?? i < defaultLegendColors.length ? defaultLegendColors[i] : undefined, + color: d.color ?? (i < defaultLegendColors.length ? defaultLegendColors[i] : undefined), dataProviderId: escapeDataProviderId( `draggable-legend-item-${uuid.v4()}-${stackByField}-${d.key}` ), From 4d6b8ce45df4a207d071f0f5c049e1962ef1822a Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Mon, 3 Aug 2020 15:05:12 -0400 Subject: [PATCH 22/74] [Monitoring] Different messaging for CPU usage alert on cloud (#74082) * Different messaging for cloud * Fix type issues --- .../monitoring/server/alerts/base_alert.ts | 5 +- .../alerts/cluster_health_alert.test.ts | 9 ++-- .../server/alerts/cpu_usage_alert.test.ts | 44 ++++++++++++++++-- .../server/alerts/cpu_usage_alert.ts | 46 ++++++++++--------- ...asticsearch_version_mismatch_alert.test.ts | 9 ++-- .../kibana_version_mismatch_alert.test.ts | 9 ++-- .../alerts/license_expiration_alert.test.ts | 9 ++-- .../logstash_version_mismatch_alert.test.ts | 9 ++-- .../server/alerts/nodes_changed_alert.test.ts | 6 ++- x-pack/plugins/monitoring/server/plugin.ts | 11 ++++- x-pack/plugins/monitoring/server/types.ts | 2 + 11 files changed, 112 insertions(+), 47 deletions(-) diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index cac57f599633d7..016acf2737f9b3 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -50,6 +50,7 @@ export class BaseAlert { protected getLogger!: (...scopes: string[]) => Logger; protected config!: MonitoringConfig; protected kibanaUrl!: string; + protected isCloud: boolean = false; protected defaultParams: CommonAlertParams | {} = {}; public get paramDetails() { return {}; @@ -82,13 +83,15 @@ export class BaseAlert { monitoringCluster: ILegacyCustomClusterClient, getLogger: (...scopes: string[]) => Logger, config: MonitoringConfig, - kibanaUrl: string + kibanaUrl: string, + isCloud: boolean ) { this.getUiSettingsService = getUiSettingsService; this.monitoringCluster = monitoringCluster; this.config = config; this.kibanaUrl = kibanaUrl; this.getLogger = getLogger; + this.isCloud = isCloud; } public getAlertType(): AlertType { diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts index f25179fa63c2fa..4b083787f58cb1 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -112,7 +112,8 @@ describe('ClusterHealthAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -175,7 +176,8 @@ describe('ClusterHealthAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -223,7 +225,8 @@ describe('ClusterHealthAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts index 2596252c92d11c..c330e977e53d8a 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts @@ -116,7 +116,8 @@ describe('CpuUsageAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -214,7 +215,8 @@ describe('CpuUsageAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -286,7 +288,8 @@ describe('CpuUsageAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -352,7 +355,8 @@ describe('CpuUsageAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -436,7 +440,8 @@ describe('CpuUsageAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -564,5 +569,34 @@ describe('CpuUsageAlert', () => { }, ]); }); + + it('should fire with different messaging for cloud', async () => { + const alert = new CpuUsageAlert(); + alert.initializeAlertType( + getUiSettingsService as any, + monitoringCluster as any, + getLogger as any, + config as any, + kibanaUrl, + true + ); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.defaultParams, + } as any); + const count = 1; + expect(scheduleActions).toHaveBeenCalledWith('default', { + internalFullMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. Verify CPU levels across affected nodes.`, + internalShortMessage: `CPU usage alert is firing for ${count} node(s) in cluster: ${clusterName}. Verify CPU levels across affected nodes.`, + action: `[View nodes](http://localhost:5601/app/monitoring#elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, + actionPlain: 'Verify CPU levels across affected nodes.', + clusterName, + count, + nodes: `${nodeName}:${cpuUsage.toFixed(2)}`, + state: 'firing', + }); + }); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts index 4742f55487045d..afe5abcf1ebd73 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts @@ -322,29 +322,31 @@ export class CpuUsageAlert extends BaseAlert { ',' )})`; const action = `[${fullActionText}](${url})`; + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.firing.internalShortMessage', + { + defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {shortActionText}`, + values: { + count: firingCount, + clusterName: cluster.clusterName, + shortActionText, + }, + } + ); + const internalFullMessage = i18n.translate( + 'xpack.monitoring.alerts.cpuUsage.firing.internalFullMessage', + { + defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {action}`, + values: { + count: firingCount, + clusterName: cluster.clusterName, + action, + }, + } + ); instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.cpuUsage.firing.internalShortMessage', - { - defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {shortActionText}`, - values: { - count: firingCount, - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.cpuUsage.firing.internalFullMessage', - { - defaultMessage: `CPU usage alert is firing for {count} node(s) in cluster: {clusterName}. {action}`, - values: { - count: firingCount, - clusterName: cluster.clusterName, - action, - }, - } - ), + internalShortMessage, + internalFullMessage: this.isCloud ? internalShortMessage : internalFullMessage, state: FIRING, nodes: firingNodes, count: firingCount, diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts index 50bf40825c515f..ed300c211215bf 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -115,7 +115,8 @@ describe('ElasticsearchVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -166,7 +167,8 @@ describe('ElasticsearchVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -214,7 +216,8 @@ describe('ElasticsearchVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts index 1a76fae9fc4207..dd3b37b5755e73 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts @@ -118,7 +118,8 @@ describe('KibanaVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -168,7 +169,8 @@ describe('KibanaVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -216,7 +218,8 @@ describe('KibanaVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts index 0f677dcc9c1205..e2f21b34efe210 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -122,7 +122,8 @@ describe('LicenseExpirationAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -195,7 +196,8 @@ describe('LicenseExpirationAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -243,7 +245,8 @@ describe('LicenseExpirationAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts index f29c199b3f1e17..fbb4a01d5b4ed2 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -115,7 +115,8 @@ describe('LogstashVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -165,7 +166,8 @@ describe('LogstashVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -213,7 +215,8 @@ describe('LogstashVersionMismatchAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts index d45d404b38304d..4b3e3d2d6cb6d4 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -128,7 +128,8 @@ describe('NodesChangedAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ @@ -180,7 +181,8 @@ describe('NodesChangedAlert', () => { monitoringCluster as any, getLogger as any, config as any, - kibanaUrl + kibanaUrl, + false ); const type = alert.getAlertType(); await type.executor({ diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 86022a0e863d5f..ed091d4b8d7a7f 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -126,10 +126,17 @@ export class Plugin { const coreStart = (await core.getStartServices())[0]; return coreStart.uiSettings; }; - + const isCloud = Boolean(plugins.cloud?.isCloudEnabled); const alerts = AlertsFactory.getAll(); for (const alert of alerts) { - alert.initializeAlertType(getUiSettingsService, cluster, this.getLogger, config, kibanaUrl); + alert.initializeAlertType( + getUiSettingsService, + cluster, + this.getLogger, + config, + kibanaUrl, + isCloud + ); plugins.alerts.registerType(alert.getAlertType()); } diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 1e7a5acb33644d..a0ef6d3e2d9842 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -17,6 +17,7 @@ import { InfraPluginSetup } from '../../infra/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server'; +import { CloudSetup } from '../../cloud/server'; export interface MonitoringLicenseService { refresh: () => Promise; @@ -44,6 +45,7 @@ export interface PluginsSetup { features: FeaturesPluginSetupContract; alerts: AlertingPluginSetupContract; infra: InfraPluginSetup; + cloud: CloudSetup; } export interface PluginsStart { From 4758cb1b4dcf7d7583b01f2f23ddb4746bf94a2c Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 3 Aug 2020 12:17:38 -0700 Subject: [PATCH 23/74] [Search] Fix telemetry collection to not send extra requests (#73382) * [Search] Fix telemetry collection to not send extra requests * Update tracker to collect duration from ES response * Fix type check * Update docs and types * Fix typescript Co-authored-by: Liza K --- ...-plugins-data-server.isearchsetup.usage.md | 2 +- .../kibana-plugin-plugins-data-server.md | 2 + ...-plugin-plugins-data-server.searchusage.md | 19 +++++++ ...gins-data-server.searchusage.trackerror.md | 15 ++++++ ...ns-data-server.searchusage.tracksuccess.md | 22 ++++++++ ...lugin-plugins-data-server.usageprovider.md | 22 ++++++++ .../collectors/create_usage_collector.test.ts | 14 ------ .../collectors/create_usage_collector.ts | 16 ------ .../data/public/search/collectors/types.ts | 2 - .../data/public/search/search_interceptor.ts | 9 +--- src/plugins/data/server/index.ts | 2 + .../data/server/search/collectors/index.ts | 20 ++++++++ .../data/server/search/collectors/routes.ts | 50 ------------------- .../data/server/search/collectors/usage.ts | 23 ++++----- .../search/es_search/es_search_strategy.ts | 27 ++++++---- src/plugins/data/server/search/index.ts | 2 + .../data/server/search/search_service.ts | 12 +++-- src/plugins/data/server/search/types.ts | 2 +- src/plugins/data/server/server.api.md | 33 ++++++++---- x-pack/plugins/data_enhanced/kibana.json | 2 +- .../public/search/search_interceptor.test.ts | 3 -- .../public/search/search_interceptor.ts | 3 -- x-pack/plugins/data_enhanced/server/plugin.ts | 15 ++++-- .../server/search/es_search_strategy.ts | 39 +++++++++++++-- 24 files changed, 214 insertions(+), 142 deletions(-) create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.trackerror.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.tracksuccess.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.usageprovider.md create mode 100644 src/plugins/data/server/search/collectors/index.ts delete mode 100644 src/plugins/data/server/search/collectors/routes.ts diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md index 85abd9d9dba980..1a94a709cc214a 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md @@ -9,5 +9,5 @@ Used internally for telemetry Signature: ```typescript -usage: SearchUsage; +usage?: SearchUsage; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 6bf481841f3347..1bcd575803f880 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -27,6 +27,7 @@ | [parseInterval(interval)](./kibana-plugin-plugins-data-server.parseinterval.md) | | | [plugin(initializerContext)](./kibana-plugin-plugins-data-server.plugin.md) | Static code to be shared externally | | [shouldReadFieldFromDocValues(aggregatable, esType)](./kibana-plugin-plugins-data-server.shouldreadfieldfromdocvalues.md) | | +| [usageProvider(core)](./kibana-plugin-plugins-data-server.usageprovider.md) | | ## Interfaces @@ -49,6 +50,7 @@ | [PluginStart](./kibana-plugin-plugins-data-server.pluginstart.md) | | | [Query](./kibana-plugin-plugins-data-server.query.md) | | | [RefreshInterval](./kibana-plugin-plugins-data-server.refreshinterval.md) | | +| [SearchUsage](./kibana-plugin-plugins-data-server.searchusage.md) | | | [TimeRange](./kibana-plugin-plugins-data-server.timerange.md) | | ## Variables diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.md new file mode 100644 index 00000000000000..d867509e915b6a --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SearchUsage](./kibana-plugin-plugins-data-server.searchusage.md) + +## SearchUsage interface + +Signature: + +```typescript +export interface SearchUsage +``` + +## Methods + +| Method | Description | +| --- | --- | +| [trackError()](./kibana-plugin-plugins-data-server.searchusage.trackerror.md) | | +| [trackSuccess(duration)](./kibana-plugin-plugins-data-server.searchusage.tracksuccess.md) | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.trackerror.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.trackerror.md new file mode 100644 index 00000000000000..212133588f62d5 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.trackerror.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SearchUsage](./kibana-plugin-plugins-data-server.searchusage.md) > [trackError](./kibana-plugin-plugins-data-server.searchusage.trackerror.md) + +## SearchUsage.trackError() method + +Signature: + +```typescript +trackError(): Promise; +``` +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.tracksuccess.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.tracksuccess.md new file mode 100644 index 00000000000000..b58f440c7dccf6 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusage.tracksuccess.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SearchUsage](./kibana-plugin-plugins-data-server.searchusage.md) > [trackSuccess](./kibana-plugin-plugins-data-server.searchusage.tracksuccess.md) + +## SearchUsage.trackSuccess() method + +Signature: + +```typescript +trackSuccess(duration: number): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| duration | number | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.usageprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.usageprovider.md new file mode 100644 index 00000000000000..ad5c61b5c85a1c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.usageprovider.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [usageProvider](./kibana-plugin-plugins-data-server.usageprovider.md) + +## usageProvider() function + +Signature: + +```typescript +export declare function usageProvider(core: CoreSetup): SearchUsage; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup | | + +Returns: + +`SearchUsage` + diff --git a/src/plugins/data/public/search/collectors/create_usage_collector.test.ts b/src/plugins/data/public/search/collectors/create_usage_collector.test.ts index a9ca9efb8b7e1a..aaaac5ae6ff7c4 100644 --- a/src/plugins/data/public/search/collectors/create_usage_collector.test.ts +++ b/src/plugins/data/public/search/collectors/create_usage_collector.test.ts @@ -90,18 +90,4 @@ describe('Search Usage Collector', () => { SEARCH_EVENT_TYPE.LONG_QUERY_RUN_BEYOND_TIMEOUT ); }); - - test('tracks response errors', async () => { - const duration = 10; - await usageCollector.trackError(duration); - expect(mockCoreSetup.http.post).toBeCalled(); - expect(mockCoreSetup.http.post.mock.calls[0][0]).toBe('/api/search/usage'); - }); - - test('tracks response duration', async () => { - const duration = 5; - await usageCollector.trackSuccess(duration); - expect(mockCoreSetup.http.post).toBeCalled(); - expect(mockCoreSetup.http.post.mock.calls[0][0]).toBe('/api/search/usage'); - }); }); diff --git a/src/plugins/data/public/search/collectors/create_usage_collector.ts b/src/plugins/data/public/search/collectors/create_usage_collector.ts index cb1b2b65c17c84..7adb0c3caa6759 100644 --- a/src/plugins/data/public/search/collectors/create_usage_collector.ts +++ b/src/plugins/data/public/search/collectors/create_usage_collector.ts @@ -72,21 +72,5 @@ export const createUsageCollector = ( SEARCH_EVENT_TYPE.LONG_QUERY_RUN_BEYOND_TIMEOUT ); }, - trackError: async (duration: number) => { - return core.http.post('/api/search/usage', { - body: JSON.stringify({ - eventType: 'error', - duration, - }), - }); - }, - trackSuccess: async (duration: number) => { - return core.http.post('/api/search/usage', { - body: JSON.stringify({ - eventType: 'success', - duration, - }), - }); - }, }; }; diff --git a/src/plugins/data/public/search/collectors/types.ts b/src/plugins/data/public/search/collectors/types.ts index bb85532fd3ab59..3e98f901eb0c39 100644 --- a/src/plugins/data/public/search/collectors/types.ts +++ b/src/plugins/data/public/search/collectors/types.ts @@ -31,6 +31,4 @@ export interface SearchUsageCollector { trackLongQueryPopupShown: () => Promise; trackLongQueryDialogDismissed: () => Promise; trackLongQueryRunBeyondTimeout: () => Promise; - trackError: (duration: number) => Promise; - trackSuccess: (duration: number) => Promise; } diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 84e24114a9e6c4..21586374d1e512 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -18,7 +18,7 @@ */ import { BehaviorSubject, throwError, timer, Subscription, defer, from, Observable } from 'rxjs'; -import { finalize, filter, tap } from 'rxjs/operators'; +import { finalize, filter } from 'rxjs/operators'; import { ApplicationStart, Toast, ToastsStart, CoreStart } from 'kibana/public'; import { getCombinedSignal, AbortError } from '../../common/utils'; import { IEsSearchRequest, IEsSearchResponse } from '../../common/search'; @@ -123,13 +123,6 @@ export class SearchInterceptor { this.pendingCount$.next(++this.pendingCount); return this.runSearch(request, combinedSignal).pipe( - tap({ - next: (e) => { - if (this.deps.usageCollector) { - this.deps.usageCollector.trackSuccess(e.rawResponse.took); - } - }, - }), finalize(() => { this.pendingCount$.next(--this.pendingCount); cleanup(); diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 461b21e1cc980c..1f3d7fbcb9f0f0 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -170,6 +170,8 @@ export { ISearchStart, getDefaultSearchParams, getTotalLoaded, + usageProvider, + SearchUsage, } from './search'; // Search namespace diff --git a/src/plugins/data/server/search/collectors/index.ts b/src/plugins/data/server/search/collectors/index.ts new file mode 100644 index 00000000000000..417dc1c2012d37 --- /dev/null +++ b/src/plugins/data/server/search/collectors/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { usageProvider, SearchUsage } from './usage'; diff --git a/src/plugins/data/server/search/collectors/routes.ts b/src/plugins/data/server/search/collectors/routes.ts deleted file mode 100644 index 38fb517e3c3f66..00000000000000 --- a/src/plugins/data/server/search/collectors/routes.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { schema } from '@kbn/config-schema'; -import { CoreSetup } from '../../../../../core/server'; -import { DataPluginStart } from '../../plugin'; -import { SearchUsage } from './usage'; - -export function registerSearchUsageRoute( - core: CoreSetup, - usage: SearchUsage -): void { - const router = core.http.createRouter(); - - router.post( - { - path: '/api/search/usage', - validate: { - body: schema.object({ - eventType: schema.string(), - duration: schema.number(), - }), - }, - }, - async (context, request, res) => { - const { eventType, duration } = request.body; - - if (eventType === 'success') usage.trackSuccess(duration); - if (eventType === 'error') usage.trackError(duration); - - return res.ok(); - } - ); -} diff --git a/src/plugins/data/server/search/collectors/usage.ts b/src/plugins/data/server/search/collectors/usage.ts index c43c572c2edbb9..e1be92aa13c37f 100644 --- a/src/plugins/data/server/search/collectors/usage.ts +++ b/src/plugins/data/server/search/collectors/usage.ts @@ -18,19 +18,18 @@ */ import { CoreSetup } from 'kibana/server'; -import { DataPluginStart } from '../../plugin'; import { Usage } from './register'; const SAVED_OBJECT_ID = 'search-telemetry'; export interface SearchUsage { - trackError(duration: number): Promise; + trackError(): Promise; trackSuccess(duration: number): Promise; } -export function usageProvider(core: CoreSetup): SearchUsage { +export function usageProvider(core: CoreSetup): SearchUsage { const getTracker = (eventType: keyof Usage) => { - return async (duration: number) => { + return async (duration?: number) => { const repository = await core .getStartServices() .then(([coreStart]) => coreStart.savedObjects.createInternalRepository()); @@ -52,17 +51,17 @@ export function usageProvider(core: CoreSetup): SearchU attributes[eventType]++; - const averageDuration = - (duration + (attributes.averageDuration ?? 0)) / - ((attributes.errorCount ?? 0) + (attributes.successCount ?? 0)); - - const newAttributes = { ...attributes, averageDuration }; + // Only track the average duration for successful requests + if (eventType === 'successCount') { + attributes.averageDuration = + ((duration ?? 0) + (attributes.averageDuration ?? 0)) / (attributes.successCount ?? 1); + } try { if (doesSavedObjectExist) { - await repository.update(SAVED_OBJECT_ID, SAVED_OBJECT_ID, newAttributes); + await repository.update(SAVED_OBJECT_ID, SAVED_OBJECT_ID, attributes); } else { - await repository.create(SAVED_OBJECT_ID, newAttributes, { id: SAVED_OBJECT_ID }); + await repository.create(SAVED_OBJECT_ID, attributes, { id: SAVED_OBJECT_ID }); } } catch (e) { // Version conflict error, swallow @@ -71,7 +70,7 @@ export function usageProvider(core: CoreSetup): SearchU }; return { - trackError: getTracker('errorCount'), + trackError: () => getTracker('errorCount')(), trackSuccess: getTracker('successCount'), }; } diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index b8010f735c3274..78ead6df1a44e6 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -20,11 +20,13 @@ import { first } from 'rxjs/operators'; import { SharedGlobalConfig, Logger } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { Observable } from 'rxjs'; +import { SearchUsage } from '../collectors/usage'; import { ISearchStrategy, getDefaultSearchParams, getTotalLoaded } from '..'; export const esSearchStrategyProvider = ( config$: Observable, - logger: Logger + logger: Logger, + usage?: SearchUsage ): ISearchStrategy => { return { search: async (context, request, options) => { @@ -43,15 +45,22 @@ export const esSearchStrategyProvider = ( ...request.params, }; - const rawResponse = (await context.core.elasticsearch.legacy.client.callAsCurrentUser( - 'search', - params, - options - )) as SearchResponse; + try { + const rawResponse = (await context.core.elasticsearch.legacy.client.callAsCurrentUser( + 'search', + params, + options + )) as SearchResponse; - // The above query will either complete or timeout and throw an error. - // There is no progress indication on this api. - return { rawResponse, ...getTotalLoaded(rawResponse._shards) }; + if (usage) usage.trackSuccess(rawResponse.took); + + // The above query will either complete or timeout and throw an error. + // There is no progress indication on this api. + return { rawResponse, ...getTotalLoaded(rawResponse._shards) }; + } catch (e) { + if (usage) usage.trackError(); + throw e; + } }, }; }; diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index 67789fcbf56b47..cea2714671f0b4 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -20,3 +20,5 @@ export { ISearchStrategy, ISearchOptions, ISearchSetup, ISearchStart } from './types'; export { getDefaultSearchParams, getTotalLoaded } from './es_search'; + +export { usageProvider, SearchUsage } from './collectors'; diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index bbd0671754749f..9dc47369567af9 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -32,7 +32,6 @@ import { UsageCollectionSetup } from '../../../usage_collection/server'; import { registerUsageCollector } from './collectors/register'; import { usageProvider } from './collectors/usage'; import { searchTelemetry } from '../saved_objects'; -import { registerSearchUsageRoute } from './collectors/routes'; import { IEsSearchRequest } from '../../common'; interface StrategyMap { @@ -51,9 +50,15 @@ export class SearchService implements Plugin { core: CoreSetup, { usageCollection }: { usageCollection?: UsageCollectionSetup } ): ISearchSetup { + const usage = usageCollection ? usageProvider(core) : undefined; + this.registerSearchStrategy( ES_SEARCH_STRATEGY, - esSearchStrategyProvider(this.initializerContext.config.legacy.globalConfig$, this.logger) + esSearchStrategyProvider( + this.initializerContext.config.legacy.globalConfig$, + this.logger, + usage + ) ); core.savedObjects.registerType(searchTelemetry); @@ -61,10 +66,7 @@ export class SearchService implements Plugin { registerUsageCollector(usageCollection, this.initializerContext); } - const usage = usageProvider(core); - registerSearchRoute(core); - registerSearchUsageRoute(core, usage); return { registerSearchStrategy: this.registerSearchStrategy, usage }; } diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 25dc890e0257db..76afd7e8c951c3 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -40,7 +40,7 @@ export interface ISearchSetup { /** * Used internally for telemetry */ - usage: SearchUsage; + usage?: SearchUsage; } export interface ISearchStart { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index d35a6a5bbb9a99..013034c79d3f31 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -32,6 +32,7 @@ import { ClusterRerouteParams } from 'elasticsearch'; import { ClusterStateParams } from 'elasticsearch'; import { ClusterStatsParams } from 'elasticsearch'; import { ConfigOptions } from 'elasticsearch'; +import { CoreSetup as CoreSetup_2 } from 'kibana/server'; import { CountParams } from 'elasticsearch'; import { CreateDocumentParams } from 'elasticsearch'; import { DeleteDocumentByQueryParams } from 'elasticsearch'; @@ -537,8 +538,7 @@ export interface ISearchOptions { // @public (undocumented) export interface ISearchSetup { registerSearchStrategy: (name: string, strategy: ISearchStrategy) => void; - // Warning: (ae-forgotten-export) The symbol "SearchUsage" needs to be exported by the entry point index.d.ts - usage: SearchUsage; + usage?: SearchUsage; } // Warning: (ae-missing-release-tag) "ISearchStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -727,6 +727,16 @@ export const search: { }; }; +// Warning: (ae-missing-release-tag) "SearchUsage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface SearchUsage { + // (undocumented) + trackError(): Promise; + // (undocumented) + trackSuccess(duration: number): Promise; +} + // Warning: (ae-missing-release-tag) "shouldReadFieldFromDocValues" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -778,6 +788,11 @@ export const UI_SETTINGS: { readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues"; }; +// Warning: (ae-missing-release-tag) "usageProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function usageProvider(core: CoreSetup_2): SearchUsage; + // Warnings were encountered during analysis: // @@ -802,13 +817,13 @@ export const UI_SETTINGS: { // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:178:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:179:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:180:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:181:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:182:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:183:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:186:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:180:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:181:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:182:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:183:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:184:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:185:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:188:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json index f0baa84afca322..637af39339e277 100644 --- a/x-pack/plugins/data_enhanced/kibana.json +++ b/x-pack/plugins/data_enhanced/kibana.json @@ -8,7 +8,7 @@ "requiredPlugins": [ "data" ], - "optionalPlugins": ["kibanaReact", "kibanaUtils"], + "optionalPlugins": ["kibanaReact", "kibanaUtils", "usageCollection"], "server": true, "ui": true, "requiredBundles": ["kibanaReact", "kibanaUtils"] diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index 9bd1ffddeaca8b..639f56d0cafca7 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -52,8 +52,6 @@ describe('EnhancedSearchInterceptor', () => { trackLongQueryPopupShown: jest.fn(), trackLongQueryDialogDismissed: jest.fn(), trackLongQueryRunBeyondTimeout: jest.fn(), - trackError: jest.fn(), - trackSuccess: jest.fn(), }; searchInterceptor = new EnhancedSearchInterceptor( @@ -458,7 +456,6 @@ describe('EnhancedSearchInterceptor', () => { expect(next.mock.calls[1][0]).toStrictEqual(timedResponses[1].value); expect(error).not.toHaveBeenCalled(); expect(mockUsageCollector.trackLongQueryRunBeyondTimeout).toBeCalledTimes(1); - expect(mockUsageCollector.trackSuccess).toBeCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index d1ed410065248b..927dc91f365b72 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -89,9 +89,6 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { // If the response indicates it is complete, stop polling and complete the observable if (!response.is_running) { - if (this.deps.usageCollector && response.rawResponse) { - this.deps.usageCollector.trackSuccess(response.rawResponse.took); - } return EMPTY; } diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index 9c3a0edf7e733e..0e9731a4141195 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -12,11 +12,17 @@ import { Logger, } from '../../../../src/core/server'; import { ES_SEARCH_STRATEGY } from '../../../../src/plugins/data/common'; -import { PluginSetup as DataPluginSetup } from '../../../../src/plugins/data/server'; +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, + usageProvider, +} from '../../../../src/plugins/data/server'; import { enhancedEsSearchStrategyProvider } from './search'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; interface SetupDependencies { data: DataPluginSetup; + usageCollection?: UsageCollectionSetup; } export class EnhancedDataServerPlugin implements Plugin { @@ -26,12 +32,15 @@ export class EnhancedDataServerPlugin implements Plugin, deps: SetupDependencies) { + const usage = deps.usageCollection ? usageProvider(core) : undefined; + deps.data.search.registerSearchStrategy( ES_SEARCH_STRATEGY, enhancedEsSearchStrategyProvider( this.initializerContext.config.legacy.globalConfig$, - this.logger + this.logger, + usage ) ); } diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 1f7b6a5f9aacea..d2a8384b1f8826 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -19,20 +19,34 @@ import { getDefaultSearchParams, getTotalLoaded, ISearchStrategy, + SearchUsage, } from '../../../../../src/plugins/data/server'; import { IEnhancedEsSearchRequest } from '../../common'; import { shimHitsTotal } from './shim_hits_total'; +import { IEsSearchResponse } from '../../../../../src/plugins/data/common/search/es_search'; -export interface AsyncSearchResponse { +interface AsyncSearchResponse { id: string; is_partial: boolean; is_running: boolean; response: SearchResponse; } +interface EnhancedEsSearchResponse extends IEsSearchResponse { + is_partial: boolean; + is_running: boolean; +} + +function isEnhancedEsSearchResponse( + response: IEsSearchResponse +): response is EnhancedEsSearchResponse { + return response.hasOwnProperty('is_partial') && response.hasOwnProperty('is_running'); +} + export const enhancedEsSearchStrategyProvider = ( config$: Observable, - logger: Logger + logger: Logger, + usage?: SearchUsage ): ISearchStrategy => { const search = async ( context: RequestHandlerContext, @@ -45,9 +59,24 @@ export const enhancedEsSearchStrategyProvider = ( const defaultParams = getDefaultSearchParams(config); const params = { ...defaultParams, ...request.params }; - return request.indexType === 'rollup' - ? rollupSearch(caller, { ...request, params }, options) - : asyncSearch(caller, { ...request, params }, options); + try { + const response = + request.indexType === 'rollup' + ? await rollupSearch(caller, { ...request, params }, options) + : await asyncSearch(caller, { ...request, params }, options); + + if ( + usage && + (!isEnhancedEsSearchResponse(response) || (!response.is_partial && !response.is_running)) + ) { + usage.trackSuccess(response.rawResponse.took); + } + + return response; + } catch (e) { + if (usage) usage.trackError(); + throw e; + } }; const cancel = async (context: RequestHandlerContext, id: string) => { From 18e2daa2fb55afb8294cf6c0ca24d69bf09710dd Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Mon, 3 Aug 2020 21:19:42 +0200 Subject: [PATCH 24/74] Fix API Keys functional test in cloud (#74075) * check admin description * split test logic --- x-pack/test/functional/apps/api_keys/home_page.ts | 15 +++++++++++---- .../test/functional/page_objects/api_keys_page.ts | 4 ++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/api_keys/home_page.ts b/x-pack/test/functional/apps/api_keys/home_page.ts index 553cceef573e14..0c4097a1d5c4ef 100644 --- a/x-pack/test/functional/apps/api_keys/home_page.ts +++ b/x-pack/test/functional/apps/api_keys/home_page.ts @@ -11,6 +11,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'apiKeys']); const log = getService('log'); const security = getService('security'); + const testSubjects = getService('testSubjects'); describe('Home page', function () { before(async () => { @@ -32,10 +33,16 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('Loads the app', async () => { await security.testUser.setRoles(['test_api_keys']); log.debug('Checking for section header'); - const headerText = await pageObjects.apiKeys.noAPIKeysHeading(); - expect(headerText).to.be('No API keys'); - const goToConsoleButton = await pageObjects.apiKeys.getGoToConsoleButton(); - expect(await goToConsoleButton.isDisplayed()).to.be(true); + const headers = await testSubjects.findAll('noApiKeysHeader'); + if (headers.length > 0) { + expect(await headers[0].getVisibleText()).to.be('No API keys'); + const goToConsoleButton = await pageObjects.apiKeys.getGoToConsoleButton(); + expect(await goToConsoleButton.isDisplayed()).to.be(true); + } else { + // page may already contain EiTable with data, then check API Key Admin text + const description = await pageObjects.apiKeys.getApiKeyAdminDesc(); + expect(description).to.be('You are an API Key administrator.'); + } }); }); }; diff --git a/x-pack/test/functional/page_objects/api_keys_page.ts b/x-pack/test/functional/page_objects/api_keys_page.ts index 17f4df74921bc4..fa10c5a574c095 100644 --- a/x-pack/test/functional/page_objects/api_keys_page.ts +++ b/x-pack/test/functional/page_objects/api_keys_page.ts @@ -14,6 +14,10 @@ export function ApiKeysPageProvider({ getService }: FtrProviderContext) { return await testSubjects.getVisibleText('noApiKeysHeader'); }, + async getApiKeyAdminDesc() { + return await testSubjects.getVisibleText('apiKeyAdminDescriptionCallOut'); + }, + async getGoToConsoleButton() { return await testSubjects.find('goToConsoleButton'); }, From 447db183e94fac36026bb27f69f3e44ac88fa694 Mon Sep 17 00:00:00 2001 From: Eric Davis Date: Mon, 3 Aug 2020 15:56:07 -0400 Subject: [PATCH 25/74] Change / to \ for Windows Enrollment command in Fleet (#74132) --- .../components/enrollment_instructions/manual/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx index a77de9369277b8..8ea236b2dd6c30 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx @@ -36,7 +36,7 @@ export const ManualInstructions: React.FunctionComponent = ({ systemctl enable elastic-agent systemctl start elastic-agent`; - const windowsCommand = `./elastic-agent enroll ${enrollArgs} + const windowsCommand = `.\elastic-agent enroll ${enrollArgs} ./install-service-elastic-agent.ps1`; return ( From 60494255e898ce69712b825299d8da55535588f2 Mon Sep 17 00:00:00 2001 From: debadair Date: Mon, 3 Aug 2020 14:29:47 -0700 Subject: [PATCH 26/74] [DOCS] Remove the ILM tutorial, which is now in the ES Ref (#73580) --- .../example-index-lifecycle-policy.asciidoc | 179 ------------------ docs/user/management.asciidoc | 2 - 2 files changed, 181 deletions(-) delete mode 100644 docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc diff --git a/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc b/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc deleted file mode 100644 index 0097bf8c648f07..00000000000000 --- a/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc +++ /dev/null @@ -1,179 +0,0 @@ -[role="xpack"] - -[[example-using-index-lifecycle-policy]] -=== Tutorial: Use {ilm-init} to manage {filebeat} time-based indices - -With {ilm} ({ilm-init}), you can create policies that perform actions automatically -on indices as they age and grow. {ilm-init} policies help you to manage -performance, resilience, and retention of your data during its lifecycle. This tutorial shows -you how to use {kib}’s *Index Lifecycle Policies* to modify and create {ilm-init} -policies. You can learn more about all of the actions, benefits, and lifecycle -phases in the {ref}/overview-index-lifecycle-management.html[{ilm-init} overview]. - - -[discrete] -[[example-using-index-lifecycle-policy-scenario]] -==== Scenario - -You’re tasked with sending syslog files to an {es} cluster. This -log data has the following data retention guidelines: - -* Keep logs on hot data nodes for 30 days -* Roll over to a new index if the size reaches 50GB -* After 30 days: -** Move the logs to warm data nodes -** Set {ref}/glossary.html#glossary-replica-shard[replica shards] to 1 -** {ref}/indices-forcemerge.html[Force merge] multiple index segments to free up the space used by deleted documents -* Delete logs after 90 days - - -[discrete] -[[example-using-index-lifecycle-policy-prerequisites]] -==== Prerequisites - -To complete this tutorial, you'll need: - -* An {es} cluster with hot and warm nodes configured for shard allocation -awareness. If you’re using {cloud}/ec-getting-started-templates-hot-warm.html[{ess}], -choose the hot-warm architecture deployment template. - -+ -For a self-managed cluster, add node attributes as described for {ref}/shard-allocation-filtering.html[shard allocation filtering] -to label data nodes as hot or warm. This step is required to migrate shards between -nodes configured with specific hardware for the hot or warm phases. -+ -For example, you can set this in your `elasticsearch.yml` for each data node: -+ -[source,yaml] --------------------------------------------------------------------------------- -node.attr.data: "warm" --------------------------------------------------------------------------------- - -* A server with {filebeat} installed and configured to send logs to the `elasticsearch` -output as described in {filebeat-ref}/filebeat-getting-started.html[Getting Started with {filebeat}]. - -[discrete] -[[example-using-index-lifecycle-policy-view-fb-ilm-policy]] -==== View the {filebeat} {ilm-init} policy - -{filebeat} includes a default {ilm-init} policy that enables rollover. {ilm-init} -is enabled automatically if you’re using the default `filebeat.yml` and index template. - -To view the default policy in {kib}, open the menu, go to *Stack Management > Data > Index Lifecycle Policies*, -search for _filebeat_, and choose the _filebeat-version_ policy. - -This policy initiates the rollover action when the index size reaches 50GB or -becomes 30 days old. - -[role="screenshot"] -image::images/tutorial-ilm-hotphaserollover-default.png["Default policy"] - - -[float] -==== Modify the policy - -The default policy is enough to prevent the creation of many tiny daily indices. -You can modify the policy to meet more complex requirements. - -. Activate the warm phase. - -+ -. Set either of the following options to control when the index moves to the warm phase: - -** Provide a value for *Timing for warm phase*. Setting this to *15* keeps the -indices on hot nodes for a range of 15-45 days, depending on when the initial -rollover occurred. - -** Enable *Move to warm phase on rollover*. The index might move to the warm phase -more quickly than intended if it reaches the *Maximum index size* before the -the *Maximum age*. - -. In the *Select a node attribute to control shard allocation* dropdown, select -*data:warm(2)* to migrate shards to warm data nodes. - -. Change *Number of replicas* to *1*. - -. Enable *Force merge data* and set *Number of segments* to *1*. -+ -NOTE: When rollover is enabled in the hot phase, action timing in the other phases -is based on the rollover date. - -+ -[role="screenshot"] -image::images/tutorial-ilm-modify-default-warm-phase-rollover.png["Modify to add warm phase"] - -. Activate the delete phase and set *Timing for delete phase* to *90* days. -+ -[role="screenshot"] -image::images/tutorial-ilm-delete-rollover.png["Add a delete phase"] - -[float] -==== Create a custom policy - -If meeting a specific retention time period is most important, you can create a -custom policy. For this option, you will use {filebeat} daily indices without -rollover. - -. To create a custom policy, open the menu, go to *Stack Management > Data > Index Lifecycle Policies*, then click -*Create policy*. - -. Activate the warm phase and configure it as follows: -+ -|=== -|*Setting* |*Value* - -|Timing for warm phase -|30 days from index creation - -|Node attribute -|`data:warm` - -|Number of replicas -|1 - -|Force merge data -|enable - -|Number of segments -|1 -|=== - -+ -[role="screenshot"] -image::images/tutorial-ilm-custom-policy.png["Modify the custom policy to add a warm phase"] - - -+ -. Activate the delete phase and set the timing. -+ -|=== -|*Setting* |*Value* -|Timing for delete phase -|90 -|=== - -+ -[role="screenshot"] -image::images/tutorial-ilm-delete-phase-creation.png["Delete phase"] - -. To configure the index to use the new policy, open the menu, then go to *Stack Management > Data > Index Lifecycle -Policies*. - -.. Find your {ilm-init} policy. -.. Click the *Actions* link next to your policy name. -.. Choose *Add policy to index template*. -.. Select your {filebeat} index template name from the *Index template* list. For example, `filebeat-7.5.x`. -.. Click *Add Policy* to save the changes. - -+ -NOTE: If you initially used the default {filebeat} {ilm-init} policy, you will -see a notice that the template already has a policy associated with it. Confirm -that you want to overwrite that configuration. - -+ -+ -TIP: When you change the policy associated with the index template, the active -index will continue to use the policy it was associated with at index creation -unless you manually update it. The next new index will use the updated policy. -For more reasons that your {ilm-init} policy changes might be delayed, see -{ref}/update-lifecycle-policy.html#update-lifecycle-policy[Update Lifecycle Policy]. diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 1704a80847652c..bc96463f6efbad 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -190,8 +190,6 @@ include::{kib-repo-dir}/management/index-lifecycle-policies/manage-policy.asciid include::{kib-repo-dir}/management/index-lifecycle-policies/add-policy-to-index.asciidoc[] -include::{kib-repo-dir}/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc[] - include::{kib-repo-dir}/management/managing-indices.asciidoc[] include::{kib-repo-dir}/management/ingest-pipelines/ingest-pipelines.asciidoc[] From 06412ae770820e6c6049e56aaccc1119d6e6f156 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Mon, 3 Aug 2020 15:43:34 -0600 Subject: [PATCH 27/74] Migrate timepicker:timeDefaults ui setting to the data plugin. (#73750) --- .../kibana/server/ui_setting_defaults.js | 14 -------------- .../state_sync/connect_to_query_state.test.ts | 2 +- .../query/timefilter/timefilter_service.ts | 2 +- .../query_bar_top_row.test.tsx | 2 +- .../query_string_input/query_bar_top_row.tsx | 2 +- src/plugins/data/server/ui_settings.ts | 18 ++++++++++++++++++ .../lens/public/app_plugin/app.test.tsx | 2 +- x-pack/plugins/monitoring/public/plugin.ts | 2 +- .../translations/translations/ja-JP.json | 6 +++--- .../translations/translations/zh-CN.json | 4 ++-- 10 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js index e1dadb0a24de15..625c2c02510db6 100644 --- a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js @@ -258,20 +258,6 @@ export function getUiSettingDefaults() { 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation', }), }, - 'timepicker:timeDefaults': { - name: i18n.translate('kbn.advancedSettings.timepicker.timeDefaultsTitle', { - defaultMessage: 'Time filter defaults', - }), - value: `{ - "from": "now-15m", - "to": "now" -}`, - type: 'json', - description: i18n.translate('kbn.advancedSettings.timepicker.timeDefaultsText', { - defaultMessage: 'The timefilter selection to use when Kibana is started without one', - }), - requiresPageReload: true, - }, 'theme:darkMode': { name: i18n.translate('kbn.advancedSettings.darkModeTitle', { defaultMessage: 'Dark mode', diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts index 307d1fe1b2b0b3..2053e0b94b21a4 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts @@ -50,7 +50,7 @@ setupMock.uiSettings.get.mockImplementation((key: string) => { return true; case UI_SETTINGS.SEARCH_QUERY_LANGUAGE: return 'kuery'; - case 'timepicker:timeDefaults': + case UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS: return { from: 'now-15m', to: 'now' }; case UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: return { pause: false, value: 0 }; diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.ts b/src/plugins/data/public/query/timefilter/timefilter_service.ts index df2fbc8e5a8f3e..35b46de5f21b25 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.ts @@ -35,7 +35,7 @@ export interface TimeFilterServiceDependencies { export class TimefilterService { public setup({ uiSettings, storage }: TimeFilterServiceDependencies): TimefilterSetup { const timefilterConfig = { - timeDefaults: uiSettings.get('timepicker:timeDefaults'), + timeDefaults: uiSettings.get(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS), refreshIntervalDefaults: uiSettings.get(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS), }; const history = new TimeHistory(storage); diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx index 5f2d4c00cd6b6f..879ff6708068e9 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx @@ -51,7 +51,7 @@ startMock.uiSettings.get.mockImplementation((key: string) => { return 'MMM D, YYYY @ HH:mm:ss.SSS'; case UI_SETTINGS.HISTORY_LIMIT: return 10; - case 'timepicker:timeDefaults': + case UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS: return { from: 'now-15m', to: 'now', diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index 86bf30ba0e3742..05249d46a1c502 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -94,7 +94,7 @@ export function QueryBarTopRow(props: Props) { } function getDateRange() { - const defaultTimeSetting = uiSettings!.get('timepicker:timeDefaults'); + const defaultTimeSetting = uiSettings!.get(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS); return { from: props.dateRangeFrom || defaultTimeSetting.from, to: props.dateRangeTo || defaultTimeSetting.to, diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index e825ef7f6c9451..763a086d7688d2 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -526,6 +526,24 @@ export function getUiSettings(): Record> { value: schema.number(), }), }, + [UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS]: { + name: i18n.translate('data.advancedSettings.timepicker.timeDefaultsTitle', { + defaultMessage: 'Time filter defaults', + }), + value: `{ + "from": "now-15m", + "to": "now" +}`, + type: 'json', + description: i18n.translate('data.advancedSettings.timepicker.timeDefaultsText', { + defaultMessage: 'The timefilter selection to use when Kibana is started without one', + }), + requiresPageReload: true, + schema: schema.object({ + from: schema.string(), + to: schema.string(), + }), + }, [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: { name: i18n.translate('data.advancedSettings.timepicker.quickRangesTitle', { defaultMessage: 'Time filter quick ranges', diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index b30a5864870091..f92343183a700e 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -196,7 +196,7 @@ describe('Lens App', () => { core.uiSettings.get.mockImplementation( jest.fn((type) => { - if (type === 'timepicker:timeDefaults') { + if (type === UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS) { return { from: 'now-7d', to: 'now' }; } else if (type === UI_SETTINGS.SEARCH_QUERY_LANGUAGE) { return 'kuery'; diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index cfac5e195a1275..88b36b9572fc7b 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -128,7 +128,7 @@ export class MonitoringPlugin UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS, JSON.stringify(refreshInterval) ); - uiSettings.overrideLocalDefault('timepicker:timeDefaults', JSON.stringify(time)); + uiSettings.overrideLocalDefault(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS, JSON.stringify(time)); } private getExternalConfig() { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f13aa374e549ca..3287352699f2ff 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -698,6 +698,8 @@ "data.advancedSettings.timepicker.refreshIntervalDefaultsText": "時間フィルターのデフォルト更新間隔「値」はミリ秒で指定する必要があります。", "data.advancedSettings.timepicker.refreshIntervalDefaultsTitle": "タイムピッカーの更新間隔", "data.advancedSettings.timepicker.thisWeek": "今週", + "data.advancedSettings.timepicker.timeDefaultsText": "時間フィルターが選択されずに Kibana が起動した際に使用される時間フィルターです", + "data.advancedSettings.timepicker.timeDefaultsTitle": "デフォルトのタイムピッカー", "data.advancedSettings.timepicker.today": "今日", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} と {lt} {to}", "data.common.kql.errors.endOfInputText": "インプットの終わり", @@ -2791,8 +2793,6 @@ "kbn.advancedSettings.storeUrlTitle": "セッションストレージに URL を格納", "kbn.advancedSettings.themeVersionText": "現在のバージョンと次のバージョンのKibanaで使用されるテーマを切り替えます。この設定を適用するにはページの更新が必要です。", "kbn.advancedSettings.themeVersionTitle": "テーマバージョン", - "kbn.advancedSettings.timepicker.timeDefaultsText": "時間フィルターが選択されずに Kibana が起動した際に使用される時間フィルターです", - "kbn.advancedSettings.timepicker.timeDefaultsTitle": "デフォルトのタイムピッカー", "kbn.advancedSettings.visualization.showRegionMapWarningsText": "用語がマップの形に合わない場合に地域マップに警告を表示するかどうかです。", "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "地域マップに警告を表示", "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "ディメンションの説明", @@ -19304,7 +19304,7 @@ "xpack.watcher.models.baseAction.simulateMessage": "アクション {id} のシミュレーションが完了しました", "xpack.watcher.models.baseAction.typeName": "アクション", "xpack.watcher.models.baseWatch.createUnknownActionTypeErrorMessage": "不明なアクションタイプ {type} を作成しようとしました。", - "xpack.watcher.models.baseWatch.idPropertyMissingBadRequestMessage": "json 引数には {id} プロパティが含まれている必要があります", + "xpack.watcher.models.baseWatch.idPropertyMissingBadRequestMessage": "json 引数には {id} プロパティが含まれている必要があります", "xpack.watcher.models.baseWatch.selectMessageText": "新規ウォッチをセットアップします。", "xpack.watcher.models.baseWatch.typeName": "ウォッチ", "xpack.watcher.models.baseWatch.watchJsonPropertyMissingBadRequestMessage": "json 引数には {watchJson} プロパティが含まれている必要があります", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 83e290b8c241ad..9598ab33ffa258 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -698,6 +698,8 @@ "data.advancedSettings.timepicker.refreshIntervalDefaultsText": "时间筛选的默认刷新时间间隔。需要使用毫秒单位指定“值”。", "data.advancedSettings.timepicker.refreshIntervalDefaultsTitle": "时间筛选刷新时间间隔", "data.advancedSettings.timepicker.thisWeek": "本周", + "data.advancedSettings.timepicker.timeDefaultsText": "未使用时间筛选启动 Kibana 时要使用的时间筛选选择", + "data.advancedSettings.timepicker.timeDefaultsTitle": "时间筛选默认值", "data.advancedSettings.timepicker.today": "今日", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} 和 {lt} {to}", "data.common.kql.errors.endOfInputText": "输入结束", @@ -2792,8 +2794,6 @@ "kbn.advancedSettings.storeUrlTitle": "将 URL 存储在会话存储中", "kbn.advancedSettings.themeVersionText": "在用于 Kibana 当前和下一版本的主题间切换。需要刷新页面,才能应用设置。", "kbn.advancedSettings.themeVersionTitle": "主题版本", - "kbn.advancedSettings.timepicker.timeDefaultsText": "未使用时间筛选启动 Kibana 时要使用的时间筛选选择", - "kbn.advancedSettings.timepicker.timeDefaultsTitle": "时间筛选默认值", "kbn.advancedSettings.visualization.showRegionMapWarningsText": "词无法联接到地图上的形状时,区域地图是否显示警告。", "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "显示区域地图警告", "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "单元格维度的解释", From 584804047a96f01d591001bd8313e0996299533e Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 3 Aug 2020 15:13:27 -0700 Subject: [PATCH 28/74] [Search] Send ID in path, rather than body (#62889) * [Search] Send ID in path, rather than body * Fix typo in merge * Update docs * Revert to using POST instead of GET * Revert accidental change Co-authored-by: Elastic Machine --- ...ugin-plugins-data-public.searchinterceptor.md | 2 +- ...ns-data-public.searchinterceptor.runsearch.md | 4 ++-- src/plugins/data/public/public.api.md | 2 +- .../data/public/search/search_interceptor.ts | 16 +++++++--------- src/plugins/data/server/search/routes.ts | 11 +++++++---- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md index 4d2fac02870356..30e980b5ffc544 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md @@ -35,7 +35,7 @@ export declare class SearchInterceptor | Method | Modifiers | Description | | --- | --- | --- | -| [runSearch(request, combinedSignal)](./kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md) | | | +| [runSearch(request, signal)](./kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md) | | | | [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given search method. Overrides the AbortSignal with one that will abort either when cancelPending is called, when the request times out, or when the original AbortSignal is aborted. Updates the pendingCount when the request is started/finalized. | | [setupTimers(options)](./kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md index 385d4f6a238d41..3601a00c48cf31 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md @@ -7,7 +7,7 @@ Signature: ```typescript -protected runSearch(request: IEsSearchRequest, combinedSignal: AbortSignal): Observable; +protected runSearch(request: IEsSearchRequest, signal: AbortSignal): Observable; ``` ## Parameters @@ -15,7 +15,7 @@ protected runSearch(request: IEsSearchRequest, combinedSignal: AbortSignal): Obs | Parameter | Type | Description | | --- | --- | --- | | request | IEsSearchRequest | | -| combinedSignal | AbortSignal | | +| signal | AbortSignal | | Returns: diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index d6812a4aa45245..2cfdab80123edc 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1661,7 +1661,7 @@ export class SearchInterceptor { // (undocumented) protected readonly requestTimeout?: number | undefined; // (undocumented) - protected runSearch(request: IEsSearchRequest, combinedSignal: AbortSignal): Observable; + protected runSearch(request: IEsSearchRequest, signal: AbortSignal): Observable; search(request: IEsSearchRequest, options?: ISearchOptions): Observable; // (undocumented) protected setupTimers(options?: ISearchOptions): { diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 21586374d1e512..e6eca16c5ca4bc 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -92,16 +92,14 @@ export class SearchInterceptor { protected runSearch( request: IEsSearchRequest, - combinedSignal: AbortSignal + signal: AbortSignal ): Observable { - return from( - this.deps.http.fetch({ - path: `/internal/search/es`, - method: 'POST', - body: JSON.stringify(request), - signal: combinedSignal, - }) - ); + const { id, ...searchRequest } = request; + const path = id != null ? `/internal/search/es/${id}` : '/internal/search/es'; + const method = 'POST'; + const body = JSON.stringify(id != null ? {} : searchRequest); + const response = this.deps.http.fetch({ path, method, body, signal }); + return from(response); } /** diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index bf1982a1f7fb2b..32d8f8c1b09e06 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -27,9 +27,12 @@ export function registerSearchRoute(core: CoreSetup): v router.post( { - path: '/internal/search/{strategy}', + path: '/internal/search/{strategy}/{id?}', validate: { - params: schema.object({ strategy: schema.string() }), + params: schema.object({ + strategy: schema.string(), + id: schema.maybe(schema.string()), + }), query: schema.object({}, { unknowns: 'allow' }), @@ -38,13 +41,13 @@ export function registerSearchRoute(core: CoreSetup): v }, async (context, request, res) => { const searchRequest = request.body; - const { strategy } = request.params; + const { strategy, id } = request.params; const signal = getRequestAbortedSignal(request.events.aborted$); const [, , selfStart] = await core.getStartServices(); try { - const response = await selfStart.search.search(context, searchRequest, { + const response = await selfStart.search.search(context, id ? { id } : searchRequest, { signal, strategy, }); From cd1b1b3a9575c4de4b503ef7f3f329b7602c9443 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 3 Aug 2020 16:15:43 -0600 Subject: [PATCH 29/74] [Maps] convert dynamic icon style to TS (#73903) * [Maps] convert dynmaic icon style to TS * tslint * fix snapsshot name changes caused by file name changes * revert change that is not part of PR Co-authored-by: Elastic Machine --- ...test.js.snap => vector_icon.test.tsx.snap} | 0 .../{breaked_legend.js => breaked_legend.tsx} | 44 +++++++--- .../legend/{category.js => category.tsx} | 15 +++- .../{circle_icon.js => circle_icon.tsx} | 4 +- .../legend/{line_icon.js => line_icon.tsx} | 4 +- .../{ordinal_legend.js => ordinal_legend.tsx} | 80 ++++++++++--------- .../{polygon_icon.js => polygon_icon.tsx} | 4 +- .../{symbol_icon.js => symbol_icon.tsx} | 27 ++++--- ...ctor_icon.test.js => vector_icon.test.tsx} | 0 .../{vector_icon.js => vector_icon.tsx} | 19 +++-- ...tyle_legend.js => vector_style_legend.tsx} | 10 ++- .../properties/dynamic_icon_property.test.tsx | 12 ++- ..._property.js => dynamic_icon_property.tsx} | 26 +++--- .../properties/dynamic_style_property.tsx | 14 +--- .../vector/properties/style_property.ts | 8 +- 15 files changed, 161 insertions(+), 106 deletions(-) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/{vector_icon.test.js.snap => vector_icon.test.tsx.snap} (100%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{breaked_legend.js => breaked_legend.tsx} (69%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{category.js => category.tsx} (82%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{circle_icon.js => circle_icon.tsx} (92%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{line_icon.js => line_icon.tsx} (77%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{ordinal_legend.js => ordinal_legend.tsx} (70%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{polygon_icon.js => polygon_icon.tsx} (78%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{symbol_icon.js => symbol_icon.tsx} (83%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{vector_icon.test.js => vector_icon.test.tsx} (100%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{vector_icon.js => vector_icon.tsx} (79%) rename x-pack/plugins/maps/public/classes/styles/vector/components/legend/{vector_style_legend.js => vector_style_legend.tsx} (74%) rename x-pack/plugins/maps/public/classes/styles/vector/properties/{dynamic_icon_property.js => dynamic_icon_property.tsx} (79%) diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.tsx.snap similarity index 100% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.tsx.snap diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.tsx similarity index 69% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.tsx index 7e8e6896ef9cef..9d5bf85005ae1a 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/breaked_legend.tsx @@ -4,35 +4,59 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Component, ReactElement } from 'react'; import _ from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; import { Category } from './category'; +import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; + const EMPTY_VALUE = ''; -export class BreakedLegend extends React.Component { - state = { +interface Break { + color: string; + label: ReactElement | string; + symbolId: string; +} + +interface Props { + style: IDynamicStyleProperty; + breaks: Break[]; + isLinesOnly: boolean; + isPointsOnly: boolean; +} + +interface State { + label: string; +} + +export class BreakedLegend extends Component { + private _isMounted: boolean = false; + + state: State = { label: EMPTY_VALUE, }; componentDidMount() { this._isMounted = true; - this._loadParams(); + this._loadLabel(); } componentDidUpdate() { - this._loadParams(); + this._loadLabel(); } componentWillUnmount() { this._isMounted = false; } - async _loadParams() { - const label = await this.props.style.getField().getLabel(); - const newState = { label }; - if (this._isMounted && !_.isEqual(this.state, newState)) { - this.setState(newState); + async _loadLabel() { + const field = this.props.style.getField(); + if (!field) { + return; + } + const label = await field.getLabel(); + if (this._isMounted && !_.isEqual(this.state.label, label)) { + this.setState({ label }); } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.tsx similarity index 82% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.tsx index cfdbd728c22176..02ca4645dd8cdc 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.tsx @@ -4,12 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { VECTOR_STYLES } from '../../../../../../common/constants'; +import React, { ReactElement } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { VECTOR_STYLES } from '../../../../../../common/constants'; import { VectorIcon } from './vector_icon'; -export function Category({ styleName, label, color, isLinesOnly, isPointsOnly, symbolId }) { +interface Props { + styleName: VECTOR_STYLES; + label: ReactElement | string; + color: string; + isLinesOnly: boolean; + isPointsOnly: boolean; + symbolId: string; +} + +export function Category({ styleName, label, color, isLinesOnly, isPointsOnly, symbolId }: Props) { function renderIcon() { if (styleName === VECTOR_STYLES.LABEL_COLOR) { return ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.tsx similarity index 92% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.tsx index 5efba64360f231..0056a2cba02cd7 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { CSSProperties } from 'react'; -export const CircleIcon = ({ style }) => ( +export const CircleIcon = ({ style }: { style: CSSProperties }) => ( ( +export const LineIcon = ({ style }: { style: CSSProperties }) => ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.tsx similarity index 70% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.tsx index 478d96962e47bb..a99548b6af7bf6 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/ordinal_legend.tsx @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; +import React, { Component, Fragment } from 'react'; import _ from 'lodash'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +// @ts-expect-error import { RangedStyleLegendRow } from '../../../components/ranged_style_legend_row'; import { VECTOR_STYLES } from '../../../../../../common/constants'; -import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import { CircleIcon } from './circle_icon'; +import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; function getLineWidthIcons() { const defaultStyle = { @@ -37,41 +39,50 @@ function getSymbolSizeIcons() { } const EMPTY_VALUE = ''; -export class OrdinalLegend extends React.Component { - constructor() { - super(); - this._isMounted = false; - this.state = { - label: EMPTY_VALUE, - }; - } +interface Props { + style: IDynamicStyleProperty; +} - async _loadParams() { - const label = await this.props.style.getField().getLabel(); - const newState = { label }; - if (this._isMounted && !_.isEqual(this.state, newState)) { - this.setState(newState); - } - } +interface State { + label: string; +} - _formatValue(value) { - if (value === EMPTY_VALUE) { - return value; - } - return this.props.style.formatField(value); +export class OrdinalLegend extends Component { + private _isMounted: boolean = false; + + state: State = { + label: EMPTY_VALUE, + }; + + componentDidMount() { + this._isMounted = true; + this._loadLabel(); } componentDidUpdate() { - this._loadParams(); + this._loadLabel(); } componentWillUnmount() { this._isMounted = false; } - componentDidMount() { - this._isMounted = true; - this._loadParams(); + async _loadLabel() { + const field = this.props.style.getField(); + if (!field) { + return; + } + const label = await field.getLabel(); + if (this._isMounted && !_.isEqual(this.state.label, label)) { + this.setState({ label }); + } + } + + _formatValue(value: string | number) { + if (value === EMPTY_VALUE) { + return value; + } + return this.props.style.formatField(value); } _renderRangeLegendHeader() { @@ -115,21 +126,16 @@ export class OrdinalLegend extends React.Component { const fieldMeta = this.props.style.getRangeFieldMeta(); - let minLabel = EMPTY_VALUE; - let maxLabel = EMPTY_VALUE; + let minLabel: string | number = EMPTY_VALUE; + let maxLabel: string | number = EMPTY_VALUE; if (fieldMeta) { - const range = { min: fieldMeta.min, max: fieldMeta.max }; - const min = this._formatValue(_.get(range, 'min', EMPTY_VALUE)); + const min = this._formatValue(_.get(fieldMeta, 'min', EMPTY_VALUE)); minLabel = - this.props.style.isFieldMetaEnabled() && range && range.isMinOutsideStdRange - ? `< ${min}` - : min; + this.props.style.isFieldMetaEnabled() && fieldMeta.isMinOutsideStdRange ? `< ${min}` : min; - const max = this._formatValue(_.get(range, 'max', EMPTY_VALUE)); + const max = this._formatValue(_.get(fieldMeta, 'max', EMPTY_VALUE)); maxLabel = - this.props.style.isFieldMetaEnabled() && range && range.isMaxOutsideStdRange - ? `> ${max}` - : max; + this.props.style.isFieldMetaEnabled() && fieldMeta.isMaxOutsideStdRange ? `> ${max}` : max; } return ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.tsx similarity index 78% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.tsx index 4210b59f0d6763..09241d538a0fe4 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { CSSProperties } from 'react'; -export const PolygonIcon = ({ style }) => ( +export const PolygonIcon = ({ style }: { style: CSSProperties }) => ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx similarity index 83% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx index ea3886c600be99..c5d41ae2b1a9b4 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx @@ -5,13 +5,24 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - +// @ts-expect-error import { getMakiSymbolSvg, styleSvg, buildSrcUrl } from '../../symbol_utils'; -export class SymbolIcon extends Component { - state = { - imgDataUrl: undefined, +interface Props { + symbolId: string; + fill?: string; + stroke?: string; +} + +interface State { + imgDataUrl: string | null; +} + +export class SymbolIcon extends Component { + private _isMounted: boolean = false; + + state: State = { + imgDataUrl: null, }; componentDidMount() { @@ -62,9 +73,3 @@ export class SymbolIcon extends Component { ); } } - -SymbolIcon.propTypes = { - symbolId: PropTypes.string.isRequired, - fill: PropTypes.string, - stroke: PropTypes.string, -}; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.tsx similarity index 100% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.tsx diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx similarity index 79% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx index e255dceda856e1..d68bbdae2c17e4 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx @@ -5,14 +5,21 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; import { CircleIcon } from './circle_icon'; import { LineIcon } from './line_icon'; import { PolygonIcon } from './polygon_icon'; import { SymbolIcon } from './symbol_icon'; -export function VectorIcon({ fillColor, isPointsOnly, isLinesOnly, strokeColor, symbolId }) { +interface Props { + fillColor?: string; + isPointsOnly: boolean; + isLinesOnly: boolean; + strokeColor?: string; + symbolId?: string; +} + +export function VectorIcon({ fillColor, isPointsOnly, isLinesOnly, strokeColor, symbolId }: Props) { if (isLinesOnly) { const style = { stroke: strokeColor, @@ -44,11 +51,3 @@ export function VectorIcon({ fillColor, isPointsOnly, isLinesOnly, strokeColor, /> ); } - -VectorIcon.propTypes = { - fillColor: PropTypes.string, - isPointsOnly: PropTypes.bool.isRequired, - isLinesOnly: PropTypes.bool.isRequired, - strokeColor: PropTypes.string, - symbolId: PropTypes.string, -}; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx similarity index 74% rename from x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx index 88eb4109627e5d..4d50c632bfd6d4 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx @@ -5,8 +5,16 @@ */ import React from 'react'; +import { IStyleProperty } from '../../properties/style_property'; -export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId }) { +interface Props { + isLinesOnly: boolean; + isPointsOnly: boolean; + styles: Array>; + symbolId: string; +} + +export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId }: Props) { const legendRows = []; for (let i = 0; i < styles.length; i++) { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx index 132c0b3f276034..81513818bc0ec9 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx @@ -21,15 +21,21 @@ import { DynamicIconProperty } from './dynamic_icon_property'; import { mockField, MockLayer } from './__tests__/test_util'; import { IconDynamicOptions } from '../../../../../common/descriptor_types'; import { IField } from '../../../fields/field'; +import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; const makeProperty = (options: Partial, field: IField = mockField) => { + const defaultOptions: IconDynamicOptions = { + iconPaletteId: null, + fieldMetaOptions: { isEnabled: false }, + }; + const mockVectorLayer = (new MockLayer() as unknown) as IVectorLayer; return new DynamicIconProperty( - { ...options, fieldMetaOptions: { isEnabled: false } }, + { ...defaultOptions, ...options }, VECTOR_STYLES.ICON, field, - new MockLayer(), + mockVectorLayer, () => { - return (x: string) => x + '_format'; + return (value: string | number | undefined) => value + '_format'; } ); }; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx similarity index 79% rename from x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx index 665317569e5e8a..0d152534aba6a4 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx @@ -6,13 +6,17 @@ import _ from 'lodash'; import React from 'react'; +import { EuiTextColor } from '@elastic/eui'; +import { Map as MbMap } from 'mapbox-gl'; import { DynamicStyleProperty } from './dynamic_style_property'; +// @ts-expect-error import { getIconPalette, getMakiIconId, getMakiSymbolAnchor } from '../symbol_utils'; import { BreakedLegend } from '../components/legend/breaked_legend'; import { getOtherCategoryLabel, assignCategoriesToPalette } from '../style_util'; -import { EuiTextColor } from '@elastic/eui'; +import { LegendProps } from './style_property'; +import { IconDynamicOptions } from '../../../../../common/descriptor_types'; -export class DynamicIconProperty extends DynamicStyleProperty { +export class DynamicIconProperty extends DynamicStyleProperty { isOrdinal() { return false; } @@ -26,7 +30,7 @@ export class DynamicIconProperty extends DynamicStyleProperty { return palette.length; } - syncIconWithMb(symbolLayerId, mbMap, iconPixelSize) { + syncIconWithMb(symbolLayerId: string, mbMap: MbMap, iconPixelSize: number) { if (this._isIconDynamicConfigComplete()) { mbMap.setLayoutProperty( symbolLayerId, @@ -64,11 +68,11 @@ export class DynamicIconProperty extends DynamicStyleProperty { }); } - _getMbIconImageExpression(iconPixelSize) { + _getMbIconImageExpression(iconPixelSize: number) { const { stops, fallbackSymbolId } = this._getPaletteStops(); if (stops.length < 1 || !fallbackSymbolId) { - //occurs when no data + // occurs when no data return null; } @@ -79,16 +83,16 @@ export class DynamicIconProperty extends DynamicStyleProperty { }); if (fallbackSymbolId) { - mbStops.push(getMakiIconId(fallbackSymbolId, iconPixelSize)); //last item is fallback style for anything that does not match provided stops + mbStops.push(getMakiIconId(fallbackSymbolId, iconPixelSize)); // last item is fallback style for anything that does not match provided stops } - return ['match', ['to-string', ['get', this._field.getName()]], ...mbStops]; + return ['match', ['to-string', ['get', this.getFieldName()]], ...mbStops]; } _getMbIconAnchorExpression() { const { stops, fallbackSymbolId } = this._getPaletteStops(); if (stops.length < 1 || !fallbackSymbolId) { - //occurs when no data + // occurs when no data return null; } @@ -99,16 +103,16 @@ export class DynamicIconProperty extends DynamicStyleProperty { }); if (fallbackSymbolId) { - mbStops.push(getMakiSymbolAnchor(fallbackSymbolId)); //last item is fallback style for anything that does not match provided stops + mbStops.push(getMakiSymbolAnchor(fallbackSymbolId)); // last item is fallback style for anything that does not match provided stops } - return ['match', ['to-string', ['get', this._field.getName()]], ...mbStops]; + return ['match', ['to-string', ['get', this.getFieldName()]], ...mbStops]; } _isIconDynamicConfigComplete() { return this._field && this._field.isValid(); } - renderLegendDetailRow({ isPointsOnly, isLinesOnly }) { + renderLegendDetailRow({ isPointsOnly, isLinesOnly }: LegendProps) { const { stops, fallbackSymbolId } = this._getPaletteStops(); const breaks = []; stops.forEach(({ stop, style }) => { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx index 216fde595af325..f666a9aad78048 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx @@ -46,7 +46,7 @@ export interface IDynamicStyleProperty extends IStyleProperty { getValueSuggestions(query: string): Promise; } -type fieldFormatter = (value: string | undefined) => string; +type fieldFormatter = (value: string | number | undefined) => string | number; export class DynamicStyleProperty extends AbstractStyleProperty implements IDynamicStyleProperty { @@ -69,12 +69,10 @@ export class DynamicStyleProperty extends AbstractStyleProperty this._getFieldFormatter = getFieldFormatter; } - // ignore TS error about "Type '(query: string) => Promise | never[]' is not assignable to type '(query: string) => Promise'." - // @ts-expect-error - getValueSuggestions = (query: string) => { + getValueSuggestions = async (query: string) => { return this._field === null ? [] - : this._field.getSource().getValueSuggestions(this._field, query); + : await this._field.getSource().getValueSuggestions(this._field, query); }; _getStyleMetaDataRequestId(fieldName: string) { @@ -306,7 +304,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty }; } - formatField(value: string | undefined): string { + formatField(value: string | number | undefined): string | number { if (this.getField()) { const fieldName = this.getFieldName(); const fieldFormatter = this._getFieldFormatter(fieldName); @@ -316,10 +314,6 @@ export class DynamicStyleProperty extends AbstractStyleProperty } } - renderLegendDetailRow() { - return null; - } - renderFieldMetaPopover(onFieldMetaOptionsChange: (fieldMetaOptions: FieldMetaOptions) => void) { if (!this.supportsFieldMeta()) { return null; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts index 7a0ed4fb3e9625..ec52f6a0f72836 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts @@ -11,7 +11,7 @@ import { getVectorStyleLabel } from '../components/get_vector_style_label'; import { FieldMetaOptions } from '../../../../../common/descriptor_types'; import { VECTOR_STYLES } from '../../../../../common/constants'; -type LegendProps = { +export type LegendProps = { isPointsOnly: boolean; isLinesOnly: boolean; symbolId?: string; @@ -20,7 +20,7 @@ type LegendProps = { export interface IStyleProperty { isDynamic(): boolean; isComplete(): boolean; - formatField(value: string | undefined): string; + formatField(value: string | number | undefined): string | number; getStyleName(): VECTOR_STYLES; getOptions(): T; renderLegendDetailRow(legendProps: LegendProps): ReactElement | null; @@ -53,7 +53,7 @@ export class AbstractStyleProperty implements IStyleProperty { return true; } - formatField(value: string | undefined): string { + formatField(value: string | number | undefined): string | number { // eslint-disable-next-line eqeqeq return value == undefined ? '' : value; } @@ -66,7 +66,7 @@ export class AbstractStyleProperty implements IStyleProperty { return this._options; } - renderLegendDetailRow() { + renderLegendDetailRow({ isPointsOnly, isLinesOnly }: LegendProps): ReactElement | null { return null; } From e00d7d279ade2ac6fedaa31f539b6b90863083e5 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Mon, 3 Aug 2020 16:03:13 -0700 Subject: [PATCH 30/74] [Reporting] Handle error gracefully when browser driver unexpectedly exits or crashes. (#71989) * [Reporting] Handle page error event directly with Puppeteer * clean up logger.error that was stringifying error * use fromEvent * better handling and error messaging if browser was closed in the middle of the screenshot pipeline * fix pdf functional api tests * fix i18n error * fix jest * fix ts * fix i18n * tweaks * ok to throw error in callback * fix ts Co-authored-by: Elastic Machine --- .../chromium/driver/chromium_driver.ts | 27 +- .../browsers/chromium/driver_factory/index.ts | 28 +- .../server/browsers/chromium/index.ts | 16 + .../csv_from_savedobject/create_job.ts | 4 +- .../printable_pdf/lib/generate_pdf.ts | 3 +- .../lib/screenshots/check_browser_open.ts | 21 + .../server/lib/screenshots/observable.test.ts | 35 +- .../server/lib/screenshots/observable.ts | 7 +- .../create_mock_browserdriverfactory.ts | 1 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../canvas_disallowed_url/data.json.gz | Bin 0 -> 883 bytes .../canvas_disallowed_url/mappings.json | 2216 +++++++++++++++++ .../test/reporting_api_integration/config.js | 9 + .../reporting/index.ts | 1 + .../reporting/network_policy.ts | 51 + 16 files changed, 2358 insertions(+), 63 deletions(-) create mode 100644 x-pack/plugins/reporting/server/lib/screenshots/check_browser_open.ts create mode 100644 x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/data.json.gz create mode 100644 x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/mappings.json create mode 100644 x-pack/test/reporting_api_integration/reporting/network_policy.ts diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index eb16a9d6de1a8c..494f7ab0a28db4 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -9,6 +9,7 @@ import { map, truncate } from 'lodash'; import open from 'opn'; import { ElementHandle, EvaluateFn, Page, Response, SerializableOrJSHandle } from 'puppeteer'; import { parse as parseUrl } from 'url'; +import { getDisallowedOutgoingUrlError } from '../'; import { LevelLogger } from '../../../lib'; import { ViewZoomWidthHeight } from '../../../lib/layouts/layout'; import { ConditionalHeaders, ElementPosition } from '../../../types'; @@ -76,6 +77,9 @@ export class HeadlessChromiumDriver { }); } + /* + * Call Page.goto and wait to see the Kibana DOM content + */ public async open( url: string, { @@ -113,6 +117,16 @@ export class HeadlessChromiumDriver { logger.info(`handled ${this.interceptedCount} page requests`); } + /* + * Let modules poll if Chrome is still running so they can short circuit if needed + */ + public isPageOpen() { + return !this.page.isClosed(); + } + + /* + * Call Page.screenshot and return a base64-encoded string of the image + */ public async screenshot(elementPosition: ElementPosition): Promise { const { boundingClientRect, scroll } = elementPosition; const screenshot = await this.page.screenshot({ @@ -220,18 +234,13 @@ export class HeadlessChromiumDriver { // We should never ever let file protocol requests go through if (!allowed || !this.allowRequest(interceptedUrl)) { - logger.error(`Got bad URL: "${interceptedUrl}", closing browser.`); await client.send('Fetch.failRequest', { errorReason: 'Aborted', requestId, }); this.page.browser().close(); - throw new Error( - i18n.translate('xpack.reporting.chromiumDriver.disallowedOutgoingUrl', { - defaultMessage: `Received disallowed outgoing URL: "{interceptedUrl}", exiting`, - values: { interceptedUrl }, - }) - ); + logger.error(getDisallowedOutgoingUrlError(interceptedUrl)); + return; } if (this._shouldUseCustomHeaders(conditionalHeaders.conditions, interceptedUrl)) { @@ -292,9 +301,9 @@ export class HeadlessChromiumDriver { } if (!allowed || !this.allowRequest(interceptedUrl)) { - logger.error(`Got disallowed URL "${interceptedUrl}", closing browser.`); this.page.browser().close(); - throw new Error(`Received disallowed URL in response: ${interceptedUrl}`); + logger.error(getDisallowedOutgoingUrlError(interceptedUrl)); + throw getDisallowedOutgoingUrlError(interceptedUrl); } }); diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 157d109e9e27e2..809bfb57dd4fab 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -19,6 +19,7 @@ import { import * as Rx from 'rxjs'; import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; +import { getChromiumDisconnectedError } from '../'; import { BROWSER_TYPE } from '../../../../common/constants'; import { CaptureConfig } from '../../../../server/types'; import { LevelLogger } from '../../../lib'; @@ -115,13 +116,19 @@ export class HeadlessChromiumDriverFactory { logger.debug(`Browser page driver created`); } catch (err) { - observer.error(new Error(`Error spawning Chromium browser: [${err}]`)); + observer.error(new Error(`Error spawning Chromium browser!`)); + observer.error(err); throw err; } const childProcess = { async kill() { - await browser.close(); + try { + await browser.close(); + } catch (err) { + // do not throw + logger.error(err); + } }, }; const { terminate$ } = safeChildProcess(logger, childProcess); @@ -167,7 +174,8 @@ export class HeadlessChromiumDriverFactory { // the unsubscribe function isn't `async` so we're going to make our best effort at // deleting the userDataDir and if it fails log an error. del(userDataDir, { force: true }).catch((error) => { - logger.error(`error deleting user data directory at [${userDataDir}]: [${error}]`); + logger.error(`error deleting user data directory at [${userDataDir}]!`); + logger.error(error); }); }); }); @@ -219,7 +227,7 @@ export class HeadlessChromiumDriverFactory { mergeMap((err) => { return Rx.throwError( i18n.translate('xpack.reporting.browsers.chromium.errorDetected', { - defaultMessage: 'Reporting detected an error: {err}', + defaultMessage: 'Reporting encountered an error: {err}', values: { err: err.toString() }, }) ); @@ -230,7 +238,7 @@ export class HeadlessChromiumDriverFactory { mergeMap((err) => { return Rx.throwError( i18n.translate('xpack.reporting.browsers.chromium.pageErrorDetected', { - defaultMessage: `Reporting detected an error on the page: {err}`, + defaultMessage: `Reporting encountered an error on the page: {err}`, values: { err: err.toString() }, }) ); @@ -238,15 +246,7 @@ export class HeadlessChromiumDriverFactory { ); const browserDisconnect$ = Rx.fromEvent(browser, 'disconnected').pipe( - mergeMap(() => - Rx.throwError( - new Error( - i18n.translate('xpack.reporting.browsers.chromium.chromiumClosed', { - defaultMessage: `Reporting detected that Chromium has closed.`, - }) - ) - ) - ) + mergeMap(() => Rx.throwError(getChromiumDisconnectedError())) ); return Rx.merge(pageError$, uncaughtExceptionPageError$, browserDisconnect$); diff --git a/x-pack/plugins/reporting/server/browsers/chromium/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/index.ts index cebcd228b01c30..29eb51dff21da0 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { BrowserDownload } from '../'; import { CaptureConfig } from '../../../server/types'; import { LevelLogger } from '../../lib'; @@ -15,3 +16,18 @@ export const chromium: BrowserDownload = { createDriverFactory: (binaryPath: string, captureConfig: CaptureConfig, logger: LevelLogger) => new HeadlessChromiumDriverFactory(binaryPath, captureConfig, logger), }; + +export const getChromiumDisconnectedError = () => + new Error( + i18n.translate('xpack.reporting.screencapture.browserWasClosed', { + defaultMessage: 'Browser was closed unexpectedly! Check the server logs for more info.', + }) + ); + +export const getDisallowedOutgoingUrlError = (interceptedUrl: string) => + new Error( + i18n.translate('xpack.reporting.chromiumDriver.disallowedOutgoingUrl', { + defaultMessage: `Received disallowed outgoing URL: "{interceptedUrl}". Failing the request and closing the browser.`, + values: { interceptedUrl }, + }) + ); diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts index e7fb0c6e2cb995..9acfc6d8c608d1 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts @@ -105,9 +105,7 @@ export const scheduleTaskFnFactory: ScheduleTaskFnFactory if (errPayload.statusCode === 404) { throw notFound(errPayload.message); } - if (err.stack) { - logger.error(err.stack); - } + logger.error(err); throw new Error(`Unable to create a job from saved object data! Error: ${err}`); }); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index f2ce423566c46a..2e0292e1f9ab58 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -90,7 +90,8 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { logger.debug(`PDF buffer byte length: ${buffer?.byteLength || 0}`); tracker.endGetBuffer(); } catch (err) { - logger.error(`Could not generate the PDF buffer! ${err}`); + logger.error(`Could not generate the PDF buffer!`); + logger.error(err); } tracker.end(); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/check_browser_open.ts b/x-pack/plugins/reporting/server/lib/screenshots/check_browser_open.ts new file mode 100644 index 00000000000000..1b5e73648c2f46 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/screenshots/check_browser_open.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HeadlessChromiumDriver } from '../../browsers'; +import { getChromiumDisconnectedError } from '../../browsers/chromium'; + +/* + * Call this function within error-handling `catch` blocks while setup and wait + * for the Kibana URL to be ready for screenshot. This detects if a block of + * code threw an exception because the page is closed or crashed. + * + * Also call once after `setup$` fires in the screenshot pipeline + */ +export const checkPageIsOpen = (browser: HeadlessChromiumDriver) => { + if (!browser.isPageOpen()) { + throw getChromiumDisconnectedError(); + } +}; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index 1b72be6c92f439..0ad41cd904853f 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -281,7 +281,7 @@ describe('Screenshot Observable Pipeline', () => { `); }); - it('recovers if exit$ fires a timeout signal', async () => { + it('observes page exit', async () => { // mocks const mockGetCreatePage = (driver: HeadlessChromiumDriver) => jest @@ -311,38 +311,7 @@ describe('Screenshot Observable Pipeline', () => { }).toPromise(); }; - await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` - Array [ - Object { - "elementsPositionAndAttributes": Array [ - Object { - "attributes": Object {}, - "position": Object { - "boundingClientRect": Object { - "height": 200, - "left": 0, - "top": 0, - "width": 200, - }, - "scroll": Object { - "x": 0, - "y": 0, - }, - }, - }, - ], - "error": "Instant timeout has fired!", - "screenshots": Array [ - Object { - "base64EncodedData": "allyourBase64", - "description": undefined, - "title": undefined, - }, - ], - "timeRange": null, - }, - ] - `); + await expect(getScreenshot()).rejects.toMatchInlineSnapshot(`"Instant timeout has fired!"`); }); it(`uses defaults for element positions and size when Kibana page is not ready`, async () => { diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts index ab4dabf9ed2c21..c6d3d826c88fb0 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts @@ -24,6 +24,7 @@ import { ScreenshotResults, ScreenshotsObservableFn, } from '../../types'; +import { checkPageIsOpen } from './check_browser_open'; import { DEFAULT_PAGELOAD_SELECTOR } from './constants'; import { getElementPositionAndAttributes } from './get_element_position_data'; import { getNumberOfItems } from './get_number_of_items'; @@ -68,7 +69,6 @@ export function screenshotsObservableFactory( return Rx.from(urls).pipe( concatMap((url, index) => { const setup$: Rx.Observable = Rx.of(1).pipe( - takeUntil(exit$), mergeMap(() => { // If we're moving to another page in the app, we'll want to wait for the app to tell us // it's loaded the next page. @@ -117,14 +117,19 @@ export function screenshotsObservableFactory( })); }), catchError((err) => { + checkPageIsOpen(driver); // if browser has closed, throw a relevant error about it + logger.error(err); return Rx.of({ elementsPositionAndAttributes: null, timeRange: null, error: err }); }) ); return setup$.pipe( + takeUntil(exit$), mergeMap( async (data: ScreenSetupData): Promise => { + checkPageIsOpen(driver); // re-check that the browser has not closed + const elements = data.elementsPositionAndAttributes ? data.elementsPositionAndAttributes : getDefaultElementPosition(layout.getViewport(1)); diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts index db10d96db22636..08313e6892f3c4 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts @@ -129,6 +129,7 @@ export const createMockBrowserDriverFactory = async ( mockBrowserDriver.evaluate = opts.evaluate ? opts.evaluate : defaultOpts.evaluate; mockBrowserDriver.screenshot = opts.screenshot ? opts.screenshot : defaultOpts.screenshot; mockBrowserDriver.open = opts.open ? opts.open : defaultOpts.open; + mockBrowserDriver.isPageOpen = () => true; mockBrowserDriverFactory.createPage = opts.getCreatePage ? opts.getCreatePage(mockBrowserDriver) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3287352699f2ff..6058644a84356a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -14321,7 +14321,6 @@ "xpack.remoteClusters.updateRemoteCluster.noRemoteClusterErrorMessage": "その名前のリモートクラスターはありません。", "xpack.remoteClusters.updateRemoteCluster.unknownRemoteClusterErrorMessage": "ES からレスポンスが返らず、クラスターを編集できません。", "xpack.reporting.breadcrumb": "レポート", - "xpack.reporting.browsers.chromium.chromiumClosed": "レポート生成時に Chromium の終了を検出しました。", "xpack.reporting.browsers.chromium.errorDetected": "レポート生成時にエラーを検出しました: {err}", "xpack.reporting.browsers.chromium.pageErrorDetected": "レポート生成時にページでエラーを検出しました: {err}", "xpack.reporting.chromiumDriver.disallowedOutgoingUrl": "無許可の着信 URL を受信しました: 「{interceptedUrl}」、終了します", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9598ab33ffa258..09cbbba248fd50 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -14326,7 +14326,6 @@ "xpack.remoteClusters.updateRemoteCluster.noRemoteClusterErrorMessage": "没有该名称的远程集群。", "xpack.remoteClusters.updateRemoteCluster.unknownRemoteClusterErrorMessage": "无法编辑集群,ES 未返回任何响应。", "xpack.reporting.breadcrumb": "报告", - "xpack.reporting.browsers.chromium.chromiumClosed": "Reporting 检测到 Chromium 已关闭。", "xpack.reporting.browsers.chromium.errorDetected": "Reporting 检测到错误:{err}", "xpack.reporting.browsers.chromium.pageErrorDetected": "Reporting 在页面上检测到错误:{err}", "xpack.reporting.chromiumDriver.disallowedOutgoingUrl": "接收到禁止的传出 URL:“{interceptedUrl}”,正在退出", diff --git a/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/data.json.gz b/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..c434eee5dd8d35749fce7d082a802c159e70af14 GIT binary patch literal 883 zcmV-(1C0D1iwFqD#}Hov17u-zVJ>QOZ*BmkR@;u-HV}RHR|x7$i$q$Ix{wqQv?z)` z7JVohAaxM%CRAZW8?X!sON=Q^lfjv`Hvgs&~GwK36TpP4+=<2L;{eL zOq6v1W;LZ54M2!V!9mE$iaIL-FfX{=c-DxEEDJz)W}NVFd(G=S%o8eBN)d8OT;?Gs zMnZ`YtP3HEP!J_BW?>-AM2+(>CBcQtH_<8CNXMX55{I5dN(wiEuv~=Fxgu+Z0+5sv zDY=Qx84aU@d4|0b6Z?AvgyZC1RxV)d^IJeoQiGF1V z6`l5aug}XnepJojXq_KMU;#Jdd(&9Ocs3s;B^9V?OWx>G|WDFFu%l z8n!nfbh}9I?^c3{M7feE5wJu_4%}OaBAz7_6N&RIV{SkMddH*v(yBW`$DD~rpMswG zeJ92%mvca54b!h2+p^B{-Bx{Krt!VDE_dm1)fEDA750`v+%*ToeCXem8N&>=FVPsM zAO-P;#;97k!xyVGZb{s}5CXoNeHx^HAAe%@^%pGpa0%;WIPcE8-RUd7J$hX?hl { + before(async () => { + await esArchiver.load(archive); // includes a canvas worksheet with an offending image URL + }); + + after(async () => { + await esArchiver.unload(archive); + }); + + it('should fail job when page voilates the network policy', async () => { + const downloadPath = await reportingAPI.postJob( + `/api/reporting/generate/printablePdf?jobParams=(layout:(dimensions:(height:720,width:1080),id:preserve_layout),objectType:'canvas%20workpad',relativeUrls:!(%2Fapp%2Fcanvas%23%2Fexport%2Fworkpad%2Fpdf%2Fworkpad-e7464259-0b75-4b8c-81c8-8422b15ff201%2Fpage%2F1),title:'My%20Canvas%20Workpad')` + ); + + // Retry the download URL until a "failed" response status is returned + const fails$: Rx.Observable = Rx.interval(100).pipe( + switchMap(() => supertest.get(downloadPath).then((response) => response.body)), + filter(({ statusCode }) => statusCode === 500), + map(({ message }) => message), + first(), + timeout(15000) + ); + + const reportFailed = await fails$.toPromise(); + + expect(reportFailed).to.match(/Reporting generation failed: Error:/); + }); + }); +} From 6f7bfd6a04146da160cbd54019db3f00be92974e Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Mon, 3 Aug 2020 16:08:03 -0700 Subject: [PATCH 31/74] Closes #74031 by adding nowrap style to the link. (#74175) --- .../shared/Links/apm/AnomalyDetectionSetupLink.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx index a80dcca4a107db..0c209b0aca9171 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx @@ -30,7 +30,10 @@ export function AnomalyDetectionSetupLink() { const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); return ( - + {ANOMALY_DETECTION_LINK_LABEL} From c7202e55cf77e5029e8f9a49d8195c00b78e35a1 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 3 Aug 2020 16:20:58 -0700 Subject: [PATCH 32/74] [kbn/optimizer] remove unneeded DisallowSyntaxPlugin (#74178) Co-authored-by: spalger --- .../disallowed_syntax.ts | 194 ------------------ .../disallowed_syntax_plugin.ts | 73 ------- .../common/disallowed_syntax_plugin/index.ts | 20 -- packages/kbn-optimizer/src/common/index.ts | 1 - packages/kbn-optimizer/src/index.ts | 1 - .../src/worker/webpack.config.ts | 3 +- 6 files changed, 1 insertion(+), 291 deletions(-) delete mode 100644 packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax.ts delete mode 100644 packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax_plugin.ts delete mode 100644 packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts diff --git a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax.ts b/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax.ts deleted file mode 100644 index aba4451622dcd1..00000000000000 --- a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax.ts +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import estree from 'estree'; - -export interface DisallowedSyntaxCheck { - name: string; - nodeType: estree.Node['type'] | Array; - test?: (n: any) => boolean | void; -} - -export const checks: DisallowedSyntaxCheck[] = [ - /** - * es2015 - */ - // https://github.com/estree/estree/blob/master/es2015.md#functions - { - name: '[es2015] generator function', - nodeType: ['FunctionDeclaration', 'FunctionExpression'], - test: (n: estree.FunctionDeclaration | estree.FunctionExpression) => !!n.generator, - }, - // https://github.com/estree/estree/blob/master/es2015.md#forofstatement - { - name: '[es2015] for-of statement', - nodeType: 'ForOfStatement', - }, - // https://github.com/estree/estree/blob/master/es2015.md#variabledeclaration - { - name: '[es2015] let/const variable declaration', - nodeType: 'VariableDeclaration', - test: (n: estree.VariableDeclaration) => n.kind === 'let' || n.kind === 'const', - }, - // https://github.com/estree/estree/blob/master/es2015.md#expressions - { - name: '[es2015] `super`', - nodeType: 'Super', - }, - // https://github.com/estree/estree/blob/master/es2015.md#expressions - { - name: '[es2015] ...spread', - nodeType: 'SpreadElement', - }, - // https://github.com/estree/estree/blob/master/es2015.md#arrowfunctionexpression - { - name: '[es2015] arrow function expression', - nodeType: 'ArrowFunctionExpression', - }, - // https://github.com/estree/estree/blob/master/es2015.md#yieldexpression - { - name: '[es2015] `yield` expression', - nodeType: 'YieldExpression', - }, - // https://github.com/estree/estree/blob/master/es2015.md#templateliteral - { - name: '[es2015] template literal', - nodeType: 'TemplateLiteral', - }, - // https://github.com/estree/estree/blob/master/es2015.md#patterns - { - name: '[es2015] destructuring', - nodeType: ['ObjectPattern', 'ArrayPattern', 'AssignmentPattern'], - }, - // https://github.com/estree/estree/blob/master/es2015.md#classes - { - name: '[es2015] class', - nodeType: [ - 'ClassDeclaration', - 'ClassExpression', - 'ClassBody', - 'MethodDefinition', - 'MetaProperty', - ], - }, - - /** - * es2016 - */ - { - name: '[es2016] exponent operator', - nodeType: 'BinaryExpression', - test: (n: estree.BinaryExpression) => n.operator === '**', - }, - { - name: '[es2016] exponent assignment', - nodeType: 'AssignmentExpression', - test: (n: estree.AssignmentExpression) => n.operator === '**=', - }, - - /** - * es2017 - */ - // https://github.com/estree/estree/blob/master/es2017.md#function - { - name: '[es2017] async function', - nodeType: ['FunctionDeclaration', 'FunctionExpression'], - test: (n: estree.FunctionDeclaration | estree.FunctionExpression) => n.async, - }, - // https://github.com/estree/estree/blob/master/es2017.md#awaitexpression - { - name: '[es2017] await expression', - nodeType: 'AwaitExpression', - }, - - /** - * es2018 - */ - // https://github.com/estree/estree/blob/master/es2018.md#statements - { - name: '[es2018] for-await-of statements', - nodeType: 'ForOfStatement', - test: (n: estree.ForOfStatement) => n.await, - }, - // https://github.com/estree/estree/blob/master/es2018.md#expressions - { - name: '[es2018] object spread properties', - nodeType: 'ObjectExpression', - test: (n: estree.ObjectExpression) => n.properties.some((p) => p.type === 'SpreadElement'), - }, - // https://github.com/estree/estree/blob/master/es2018.md#template-literals - { - name: '[es2018] tagged template literal with invalid escape', - nodeType: 'TemplateElement', - test: (n: estree.TemplateElement) => n.value.cooked === null, - }, - // https://github.com/estree/estree/blob/master/es2018.md#patterns - { - name: '[es2018] rest properties', - nodeType: 'ObjectPattern', - test: (n: estree.ObjectPattern) => n.properties.some((p) => p.type === 'RestElement'), - }, - - /** - * es2019 - */ - // https://github.com/estree/estree/blob/master/es2019.md#catchclause - { - name: '[es2019] catch clause without a binding', - nodeType: 'CatchClause', - test: (n: estree.CatchClause) => !n.param, - }, - - /** - * es2020 - */ - // https://github.com/estree/estree/blob/master/es2020.md#bigintliteral - { - name: '[es2020] bigint literal', - nodeType: 'Literal', - test: (n: estree.Literal) => typeof n.value === 'bigint', - }, - - /** - * webpack transforms import/export in order to support tree shaking and async imports - * - * // https://github.com/estree/estree/blob/master/es2020.md#importexpression - * { - * name: '[es2020] import expression', - * nodeType: 'ImportExpression', - * }, - * // https://github.com/estree/estree/blob/master/es2020.md#exportalldeclaration - * { - * name: '[es2020] export all declaration', - * nodeType: 'ExportAllDeclaration', - * }, - * - */ -]; - -export const checksByNodeType = new Map(); -for (const check of checks) { - const nodeTypes = Array.isArray(check.nodeType) ? check.nodeType : [check.nodeType]; - for (const nodeType of nodeTypes) { - if (!checksByNodeType.has(nodeType)) { - checksByNodeType.set(nodeType, []); - } - checksByNodeType.get(nodeType)!.push(check); - } -} diff --git a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax_plugin.ts b/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax_plugin.ts deleted file mode 100644 index 8fb7559f3e22f4..00000000000000 --- a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax_plugin.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import webpack from 'webpack'; -import acorn from 'acorn'; -import * as AcornWalk from 'acorn-walk'; - -import { checksByNodeType, DisallowedSyntaxCheck } from './disallowed_syntax'; -import { parseFilePath } from '../parse_path'; - -export class DisallowedSyntaxPlugin { - apply(compiler: webpack.Compiler) { - compiler.hooks.normalModuleFactory.tap(DisallowedSyntaxPlugin.name, (factory) => { - factory.hooks.parser.for('javascript/auto').tap(DisallowedSyntaxPlugin.name, (parser) => { - parser.hooks.program.tap(DisallowedSyntaxPlugin.name, (program: acorn.Node) => { - const module = parser.state?.current; - if (!module || !module.resource) { - return; - } - - const resource: string = module.resource; - const { dirs } = parseFilePath(resource); - - if (!dirs.includes('node_modules')) { - return; - } - - const failedChecks = new Set(); - - AcornWalk.full(program, (node) => { - const checks = checksByNodeType.get(node.type as any); - if (!checks) { - return; - } - - for (const check of checks) { - if (!check.test || check.test(node)) { - failedChecks.add(check); - } - } - }); - - if (!failedChecks.size) { - return; - } - - // throw an error to trigger a parse failure, causing this module to be reported as invalid - throw new Error( - `disallowed syntax found in file ${resource}:\n - ${Array.from(failedChecks) - .map((c) => c.name) - .join('\n - ')}` - ); - }); - }); - }); - } -} diff --git a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts b/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts deleted file mode 100644 index ca5ba1b90fe954..00000000000000 --- a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './disallowed_syntax_plugin'; diff --git a/packages/kbn-optimizer/src/common/index.ts b/packages/kbn-optimizer/src/common/index.ts index 89cde2c1cd064f..5f17a9b38f9fb2 100644 --- a/packages/kbn-optimizer/src/common/index.ts +++ b/packages/kbn-optimizer/src/common/index.ts @@ -27,6 +27,5 @@ export * from './ts_helpers'; export * from './rxjs_helpers'; export * from './array_helpers'; export * from './event_stream_helpers'; -export * from './disallowed_syntax_plugin'; export * from './parse_path'; export * from './theme_tags'; diff --git a/packages/kbn-optimizer/src/index.ts b/packages/kbn-optimizer/src/index.ts index 29922944e88172..39cf2120baf0a1 100644 --- a/packages/kbn-optimizer/src/index.ts +++ b/packages/kbn-optimizer/src/index.ts @@ -20,5 +20,4 @@ export { OptimizerConfig } from './optimizer'; export * from './run_optimizer'; export * from './log_optimizer_state'; -export * from './common/disallowed_syntax_plugin'; export * from './report_optimizer_stats'; diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index ae5d2b5fb32922..820b13629697d3 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -29,7 +29,7 @@ import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import CompressionPlugin from 'compression-webpack-plugin'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; -import { Bundle, BundleRefs, WorkerConfig, parseDirPath, DisallowedSyntaxPlugin } from '../common'; +import { Bundle, BundleRefs, WorkerConfig, parseDirPath } from '../common'; import { BundleRefsPlugin } from './bundle_refs_plugin'; const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; @@ -70,7 +70,6 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: plugins: [ new CleanWebpackPlugin(), - new DisallowedSyntaxPlugin(), new BundleRefsPlugin(bundle, bundleRefs), ...(bundle.banner ? [new webpack.BannerPlugin({ banner: bundle.banner, raw: true })] : []), ], From 47d6be3b7ecc4dedb232615d3aa9d7fa4f16bbf2 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Mon, 3 Aug 2020 18:36:01 -0500 Subject: [PATCH 33/74] [Metrics UI] Fix pluralization in Alert Preview (#74160) --- .../public/alerting/common/components/alert_preview.tsx | 7 ++----- x-pack/plugins/translations/translations/ja-JP.json | 4 ++-- x-pack/plugins/translations/translations/zh-CN.json | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx index d5b50fce387183..9d28ef71a5518f 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx @@ -162,11 +162,10 @@ export const AlertPreview: React.FC = (props) => { {' '} @@ -187,11 +186,9 @@ export const AlertPreview: React.FC = (props) => { {showNoDataResults && previewResult.resultTotals.noData ? ( {previewResult.resultTotals.noData}, - plural: previewResult.resultTotals.noData !== 1 ? 's' : '', }} /> ) : null}{' '} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6058644a84356a..544f6cc6ec342a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8830,9 +8830,9 @@ "xpack.infra.metrics.alertFlyout.alertOnNoData": "データがない場合に通知する", "xpack.infra.metrics.alertFlyout.alertPreviewError": "このアラート条件をプレビューするときにエラーが発生しました", "xpack.infra.metrics.alertFlyout.alertPreviewErrorResult": "一部のデータを評価するときにエラーが発生しました。", - "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups} {groupName}{plural}", + "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups} {groupName}", "xpack.infra.metrics.alertFlyout.alertPreviewGroupsAcross": "すべてを対象にする", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "データがない{were} {noData}結果{plural}がありました。", + "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "データがない {noData}結果がありました。", "xpack.infra.metrics.alertFlyout.alertPreviewResult": "このアラートは{firedTimes}回発生しました", "xpack.infra.metrics.alertFlyout.alertPreviewResultLookback": "過去{lookback}", "xpack.infra.metrics.alertFlyout.conditions": "条件", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 09cbbba248fd50..2ee85fc87cc728 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8832,9 +8832,9 @@ "xpack.infra.metrics.alertFlyout.alertOnNoData": "没数据时提醒我", "xpack.infra.metrics.alertFlyout.alertPreviewError": "尝试预览此告警条件时发生错误", "xpack.infra.metrics.alertFlyout.alertPreviewErrorResult": "尝试评估部分数据时发生错误。", - "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups} 个{groupName}{plural}", + "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups} 个{groupName}", "xpack.infra.metrics.alertFlyout.alertPreviewGroupsAcross": "在", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "存在{were} {noData} 个无数据结果{plural}。", + "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "存在 {noData} 个无数据结果。", "xpack.infra.metrics.alertFlyout.alertPreviewResult": "此告警将发生 {firedTimes}", "xpack.infra.metrics.alertFlyout.alertPreviewResultLookback": "在过去 {lookback}。", "xpack.infra.metrics.alertFlyout.conditions": "条件", From 3ea9e607e5481bea06cab6b206e6c98d5ed88aed Mon Sep 17 00:00:00 2001 From: Ross Wolf <31489089+rw-access@users.noreply.github.com> Date: Mon, 3 Aug 2020 16:38:03 -0700 Subject: [PATCH 34/74] [Detection Rules] Add 7.9 rules (#74177) --- .../collection_cloudtrail_logging_created.json | 5 ++++- ...and_and_control_certutil_network_connection.json | 3 ++- ...dential_access_attempted_bypass_of_okta_mfa.json | 5 ++++- ...tial_access_aws_iam_assume_role_brute_force.json | 5 ++++- ...redential_access_credential_dumping_msbuild.json | 3 ++- ...redential_access_iam_user_addition_to_group.json | 5 ++++- ...ccess_okta_brute_force_or_password_spraying.json | 5 ++++- ...ential_access_secretsmanager_getsecretvalue.json | 9 ++++++--- .../credential_access_tcpdump_activity.json | 3 ++- ...he_hidden_file_attribute_with_via_attribexe.json | 3 ++- ...ion_attempt_to_disable_iptables_or_firewall.json | 3 ++- ...e_evasion_attempt_to_disable_syslog_service.json | 3 ++- ...e16_or_base32_encoding_or_decoding_activity.json | 3 ++- ...vasion_base64_encoding_or_decoding_activity.json | 3 ++- ...defense_evasion_clearing_windows_event_logs.json | 3 ++- .../defense_evasion_cloudtrail_logging_deleted.json | 5 ++++- ...efense_evasion_cloudtrail_logging_suspended.json | 5 ++++- .../defense_evasion_cloudwatch_alarm_deletion.json | 5 ++++- ...efense_evasion_config_service_rule_deletion.json | 5 ++++- ...ense_evasion_configuration_recorder_stopped.json | 5 ++++- ...asion_delete_volume_usn_journal_with_fsutil.json | 3 ++- ...asion_deleting_backup_catalogs_with_wbadmin.json | 3 ++- ...asion_deletion_of_bash_command_line_history.json | 3 ++- .../defense_evasion_disable_selinux_attempt.json | 3 ++- ...n_disable_windows_firewall_rules_with_netsh.json | 3 ++- .../defense_evasion_ec2_flow_log_deletion.json | 5 ++++- .../defense_evasion_ec2_network_acl_deletion.json | 5 ++++- ...ion_encoding_or_decoding_files_via_certutil.json | 3 ++- ...ion_execution_msbuild_started_by_office_app.json | 3 ++- ...evasion_execution_msbuild_started_by_script.json | 3 ++- ...execution_msbuild_started_by_system_process.json | 3 ++- ...e_evasion_execution_msbuild_started_renamed.json | 3 ++- ...on_execution_msbuild_started_unusal_process.json | 3 ++- .../defense_evasion_file_deletion_via_shred.json | 3 ++- .../defense_evasion_file_mod_writable_dir.json | 3 ++- ...defense_evasion_guardduty_detector_deletion.json | 5 ++++- ...e_evasion_hex_encoding_or_decoding_activity.json | 3 ++- .../defense_evasion_hidden_file_dir_tmp.json | 3 ++- .../defense_evasion_kernel_module_removal.json | 3 ++- ...sion_misc_lolbin_connecting_to_the_internet.json | 3 ++- ...defense_evasion_modification_of_boot_config.json | 3 ++- ...se_evasion_s3_bucket_configuration_deletion.json | 5 ++++- ...on_volume_shadow_copy_deletion_via_vssadmin.json | 3 ++- ...vasion_volume_shadow_copy_deletion_via_wmic.json | 3 ++- .../defense_evasion_waf_acl_deletion.json | 5 ++++- ...nse_evasion_waf_rule_or_rule_group_deletion.json | 5 ++++- .../discovery_kernel_module_enumeration.json | 3 ++- .../discovery_net_command_system_account.json | 3 ++- .../discovery_virtual_machine_fingerprinting.json | 3 ++- .../discovery_whoami_commmand.json | 3 ++- .../rules/prepackaged_rules/elastic_endpoint.json | 13 ++++++++++--- ...n_command_prompt_connecting_to_the_internet.json | 3 ++- ...ecution_command_shell_started_by_powershell.json | 3 ++- .../execution_command_shell_started_by_svchost.json | 3 ++- ...ecutable_program_connecting_to_the_internet.json | 3 ++- .../execution_local_service_commands.json | 3 ++- ...xecution_msbuild_making_network_connections.json | 3 ++- .../execution_mshta_making_network_connections.json | 3 ++- .../prepackaged_rules/execution_msxsl_network.json | 3 ++- .../prepackaged_rules/execution_perl_tty_shell.json | 3 ++- .../execution_psexec_lateral_movement_command.json | 3 ++- .../execution_python_tty_shell.json | 3 ++- ...r_server_program_connecting_to_the_internet.json | 3 ++- .../execution_script_executing_powershell.json | 3 ++- ...xecution_suspicious_ms_office_child_process.json | 3 ++- ...ecution_suspicious_ms_outlook_child_process.json | 3 ++- .../execution_suspicious_pdf_reader.json | 3 ++- ...ion_unusual_network_connection_via_rundll32.json | 3 ++- ...xecution_unusual_process_network_connection.json | 3 ++- .../execution_via_net_com_assemblies.json | 3 ++- .../execution_via_system_manager.json | 5 ++++- .../exfiltration_ec2_snapshot_change_activity.json | 5 ++++- .../rules/prepackaged_rules/external_alerts.json | 8 ++++++-- .../impact_attempt_to_revoke_okta_api_token.json | 5 ++++- .../impact_cloudtrail_logging_updated.json | 5 ++++- .../impact_cloudwatch_log_group_deletion.json | 5 ++++- .../impact_cloudwatch_log_stream_deletion.json | 5 ++++- .../impact_ec2_disable_ebs_encryption.json | 5 ++++- .../impact_iam_deactivate_mfa_device.json | 5 ++++- .../impact_iam_group_deletion.json | 5 ++++- .../impact_possible_okta_dos_attack.json | 5 ++++- .../impact_rds_cluster_deletion.json | 5 ++++- .../impact_rds_instance_cluster_stoppage.json | 5 ++++- .../initial_access_console_login_root.json | 5 ++++- .../initial_access_password_recovery.json | 5 ++++- ...s_suspicious_activity_reported_by_okta_user.json | 5 ++++- ...ral_movement_direct_outbound_smb_connection.json | 3 ++- ...l_movement_telnet_network_activity_external.json | 3 ++- ...l_movement_telnet_network_activity_internal.json | 3 ++- .../prepackaged_rules/linux_hping_activity.json | 3 ++- .../prepackaged_rules/linux_iodine_activity.json | 3 ++- .../prepackaged_rules/linux_mknod_activity.json | 3 ++- .../linux_netcat_network_connection.json | 3 ++- .../prepackaged_rules/linux_nmap_activity.json | 3 ++- .../prepackaged_rules/linux_nping_activity.json | 3 ++- .../linux_process_started_in_temp_directory.json | 3 ++- .../prepackaged_rules/linux_socat_activity.json | 3 ++- .../prepackaged_rules/linux_strace_activity.json | 3 ++- .../okta_attempt_to_deactivate_okta_mfa_rule.json | 5 ++++- .../okta_attempt_to_delete_okta_policy.json | 5 ++++- .../okta_attempt_to_modify_okta_mfa_rule.json | 5 ++++- .../okta_attempt_to_modify_okta_network_zone.json | 5 ++++- .../okta_attempt_to_modify_okta_policy.json | 5 ++++- ...modify_or_delete_application_sign_on_policy.json | 5 ++++- .../okta_threat_detected_by_okta_threatinsight.json | 5 ++++- ...nistrator_privileges_assigned_to_okta_group.json | 5 ++++- .../persistence_adobe_hijack_persistence.json | 3 ++- ...ersistence_attempt_to_create_okta_api_token.json | 5 ++++- ...mpt_to_deactivate_mfa_for_okta_user_account.json | 5 ++++- ...rsistence_attempt_to_deactivate_okta_policy.json | 5 ++++- ..._to_reset_mfa_factors_for_okta_user_account.json | 5 ++++- .../persistence_ec2_network_acl_creation.json | 5 ++++- .../persistence_iam_group_creation.json | 5 ++++- .../persistence_kernel_module_activity.json | 3 ++- .../persistence_local_scheduled_task_commands.json | 3 ++- .../persistence_rds_cluster_creation.json | 5 ++++- .../persistence_shell_activity_by_web_server.json | 3 ++- .../persistence_system_shells_via_services.json | 3 ++- .../persistence_user_account_creation.json | 3 ++- ...privilege_escalation_root_login_without_mfa.json | 5 ++++- ...ivilege_escalation_setgid_bit_set_via_chmod.json | 3 ++- ...ivilege_escalation_setuid_bit_set_via_chmod.json | 3 ++- .../privilege_escalation_sudoers_file_mod.json | 3 ++- ...rivilege_escalation_uac_bypass_event_viewer.json | 3 ++- ...escalation_unusual_parentchild_relationship.json | 3 ++- ...privilege_escalation_updateassumerolepolicy.json | 5 ++++- 126 files changed, 364 insertions(+), 131 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json index ee39661ee9b10a..acc6f7724d0b5e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json @@ -25,7 +25,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json index 4132d03c278545..25274928aa2b7a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json @@ -4,7 +4,8 @@ ], "description": "Identifies certutil.exe making a network connection. Adversaries could abuse certutil.exe to download a certificate, or malware, from a remote URL.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json index eb8523b797ddf6..a2267755c73766 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json @@ -20,7 +20,10 @@ "severity": "high", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json index ddc9e917821361..4c79be6fe90455 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json @@ -21,7 +21,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json index a2936f3f09519b..6be1f037f967e1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json index ecbf268550b6cc..5b73f849dddc5a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json @@ -24,7 +24,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json index 87f20525203f6b..2cf24d54b72686 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json @@ -23,7 +23,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json index f570b7fb3e946b..ef20746fb1d805 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json @@ -21,12 +21,15 @@ "https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html", "http://detectioninthe.cloud/credential_access/access_secret_in_secrets_manager/" ], - "risk_score": 21, + "risk_score": 73, "rule_id": "a00681e3-9ed6-447c-ab2c-be648821c622", - "severity": "low", + "severity": "high", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Data Protection", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json index 9abbe3de148ddb..d5b069f7b81e79 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json @@ -7,7 +7,8 @@ "Some normal use of this command may originate from server or network administrators engaged in network troubleshooting." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json index 861821d24b73ce..b22b74ebc53bc2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json @@ -4,7 +4,8 @@ ], "description": "Adversaries can add the 'hidden' attribute to files to hide them from the user in an attempt to evade detection.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json index 431d133845f0e7..e2ba81da917b3e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json @@ -4,7 +4,8 @@ ], "description": "Adversaries may attempt to disable the iptables or firewall service in an attempt to affect how a host is allowed to receive or send network traffic.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json index 13dd405c793265..4f4a9aacd79aae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json @@ -4,7 +4,8 @@ ], "description": "Adversaries may attempt to disable the syslog service in an attempt to an attempt to disrupt event logging and evade detection by security controls.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json index 67fb0b2e6755ac..5bcc4a00ccd820 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json @@ -7,7 +7,8 @@ "Automated tools such as Jenkins may encode or decode files as part of their normal behavior. These events can be filtered by the process executable or username values." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json index f60dede360b4b7..a17fd6d2702ddc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json @@ -7,7 +7,8 @@ "Automated tools such as Jenkins may encode or decode files as part of their normal behavior. These events can be filtered by the process executable or username values." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json index 7c6ede8df73466..cf09bc512916fa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json @@ -4,7 +4,8 @@ ], "description": "Identifies attempts to clear Windows event log stores. This is often done by attackers in an attempt to evade detection or destroy forensic evidence on a system.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json index 78f4c9e853f64e..b5e76b6ebfa36a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json index f412ad9b2e2fd3..6ba9503edc2602 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json index b76ea0944f8555..3d31eead43c8f1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json index 353067e6db8337..22ceb35dfc8569 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json index b70aa5cd11b520..95e357e56fe3b0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json @@ -25,7 +25,10 @@ "severity": "high", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json index ba9f43651e32fe..0c82444dd9397b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of the fsutil.exe to delete the volume USNJRNL. This technique is used by attackers to eliminate evidence of files created during post-exploitation activities.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json index 79c2d4c25b7d59..c76c5f20fa88bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of the wbadmin.exe to delete the backup catalog. Ransomware and other malware may do this to prevent system recovery.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json index b9727e18dddcfc..b38ed94e132e1f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json @@ -4,7 +4,8 @@ ], "description": "Adversaries may attempt to clear the bash command line history in an attempt to evade detection or forensic investigations.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "lucene", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json index e8f5f1a8de1c59..229a03de396008 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json @@ -4,7 +4,8 @@ ], "description": "Identifies potential attempts to disable Security-Enhanced Linux (SELinux), which is a Linux kernel security feature to support access control policies. Adversaries may disable security tools to avoid possible detection of their tools and activities.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json index 2b45f059ec8d90..4800e87c180e2a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of the netsh.exe to disable or weaken the local firewall. Attackers will use this command line tool to disable the firewall during troubleshooting or to enable network mobility.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json index a1b0ec0f01d2ab..809a9a187937dc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json @@ -25,7 +25,10 @@ "severity": "high", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json index 21ce4e498ccaf1..8467b87f9983cc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json @@ -27,7 +27,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Network", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json index 056de9e5c003e4..075dd13d9819b6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json @@ -4,7 +4,8 @@ ], "description": "Identifies the use of certutil.exe to encode or decode data. CertUtil is a native Windows component which is part of Certificate Services. CertUtil is often abused by attackers to encode or decode base64 data for stealthier command and control or exfiltration.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json index 814caee4e888a5..133863f8e2148f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual. It is quite unusual for this program to be started by an Office application like Word or Excel." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json index 6426f8722df3d1..85d348bb14be02 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json index b27dfced0f4f6a..38482c0a70fc94 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json index d7da758e57c6db..7db683caf2bb22 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json index 30d482e9b95696..1c4666955dde08 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json @@ -7,7 +7,8 @@ "The Build Engine is commonly used by Windows developers but use by non-engineers is unusual. If a build system triggers this rule it can be exempted by process, user or host name." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json index 4aad56abd0534c..c375ea7b19b376 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json @@ -4,7 +4,8 @@ ], "description": "Malware or other files dropped or created on a system by an adversary may leave traces behind as to what was done within a network and how. Adversaries may remove these files over the course of an intrusion to keep their footprint low or remove them at the end as part of the post-intrusion cleanup process.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json index c630ad1eecec09..22090e1a241e7b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json @@ -7,7 +7,8 @@ "Certain programs or applications may modify files or change ownership in writable directories. These can be exempted by username." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json index 989eff90aaf025..c2590a2f062cc2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json @@ -25,7 +25,10 @@ "severity": "high", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json index 3c1ea7ee229c99..00491937e9aae3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json @@ -7,7 +7,8 @@ "Automated tools such as Jenkins may encode or decode files as part of their normal behavior. These events can be filtered by the process executable or username values." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json index 7202d9be3b8c38..16a398011fc536 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json @@ -7,7 +7,8 @@ "Certain tools may create hidden temporary files or directories upon installation or as part of their normal behavior. These events can be filtered by the process arguments, username, or process name values." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "lucene", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json index f055ee44efb395..11781cb7195999 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json @@ -7,7 +7,8 @@ "There is usually no reason to remove modules, but some buggy modules require it. These can be exempted by username. Note that some Linux distributions are not built to support the removal of modules at all." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json index afa1467b15074b..7d931725fa6ebb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json @@ -4,7 +4,8 @@ ], "description": "Binaries signed with trusted digital certificates can execute on Windows systems protected by digital signature validation. Adversaries may use these binaries to 'live off the land' and execute malicious files that could bypass application allowlists and signature validation.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json index 801b60a2572e23..1bffe7a1cfc248 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of bcdedit.exe to delete boot configuration data. This tactic is sometimes used as by malware or an attacker as a destructive technique.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json index b1e8d0cd0d3e1d..13d0eb267f6408 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json @@ -28,7 +28,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Asset Visibility", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json index 3166cc23ae7261..f3cc5c2eec8a32 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of vssadmin.exe for shadow copy deletion on endpoints. This commonly occurs in tandem with ransomware or other destructive attacks.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json index 730879684a8113..334276142ca428 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of wmic.exe for shadow copy deletion on endpoints. This commonly occurs in tandem with ransomware or other destructive attacks.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json index b2092dc78b0128..ef7667e34be3af 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Network", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json index ccec76b7f79747..1e8e1bfa422466 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Network", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json index 14472f02280a34..0e4bea426c5913 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json @@ -7,7 +7,8 @@ "Security tools and device drivers may run these programs in order to enumerate kernel modules. Use of these programs by ordinary users is uncommon. These can be exempted by process name or username." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json index a2fe82c43b15ab..6ac2bbf355961b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json @@ -4,7 +4,8 @@ ], "description": "Identifies the SYSTEM account using the Net utility. The Net utility is a component of the Windows operating system. It is used in command line operations for control of users, groups, services, and network connections.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json index 94f09f73b454ee..e73aa5f4566a7a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json @@ -7,7 +7,8 @@ "Certain tools or automated software may enumerate hardware information. These tools can be exempted via user name or process arguments to eliminate potential noise." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json index a7833c4a017511..00171867871390 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json @@ -7,7 +7,8 @@ "Security testing tools and frameworks may run this command. Some normal use of this command may originate from automation tools and frameworks." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json index 05601ec8ffb4c1..1466b4526815b2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json @@ -1,5 +1,7 @@ { - "author": ["Elastic"], + "author": [ + "Elastic" + ], "description": "Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Elastic Endpoint alerts.", "enabled": true, "exceptions_list": [ @@ -11,7 +13,9 @@ } ], "from": "now-10m", - "index": ["logs-endpoint.alerts-*"], + "index": [ + "logs-endpoint.alerts-*" + ], "language": "kuery", "license": "Elastic License", "max_signals": 10000, @@ -54,7 +58,10 @@ "value": "99" } ], - "tags": ["Elastic", "Endpoint"], + "tags": [ + "Elastic", + "Endpoint" + ], "timestamp_override": "event.ingested", "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json index 97197be498a8df..0ba6480fe42a10 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json @@ -7,7 +7,8 @@ "Administrators may use the command prompt for regular administrative tasks. It's important to baseline your environment for network connections being made from the command prompt to determine any abnormal use of this tool." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json index 832ca1e1e7d399..2d3edb0f5f6cce 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json @@ -4,7 +4,8 @@ ], "description": "Identifies a suspicious parent child process relationship with cmd.exe descending from PowerShell.exe.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json index e92ee45c0f3b63..3a4b4915f3c8b7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json @@ -4,7 +4,8 @@ ], "description": "Identifies a suspicious parent child process relationship with cmd.exe descending from svchost.exe", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json index c75f77301e531e..a2eb76b9831f0b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json @@ -4,7 +4,8 @@ ], "description": "Compiled HTML files (.chm) are commonly distributed as part of the Microsoft HTML Help system. Adversaries may conceal malicious code in a CHM file and deliver it to a victim for execution. CHM content is loaded by the HTML Help executable program (hh.exe).", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json index 9b50d99761ad27..e43ab9de86ef76 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json @@ -4,7 +4,8 @@ ], "description": "Identifies use of sc.exe to create, modify, or start services on remote hosts. This could be indicative of adversary lateral movement but will be noisy if commonly done by admins.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json index 192e35df1da3f2..9d480259d49de5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json @@ -4,7 +4,8 @@ ], "description": "Identifies MsBuild.exe making outbound network connections. This may indicate adversarial activity as MsBuild is often leveraged by adversaries to execute code and evade detection.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json index cb098086e33249..cdef5f16e5cd76 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json @@ -4,7 +4,8 @@ ], "description": "Identifies mshta.exe making a network connection. This may indicate adversarial activity as mshta.exe is often leveraged by adversaries to execute malicious scripts and evade detection.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json index 9f1d2fc62fadff..d501bda08c3a54 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json @@ -4,7 +4,8 @@ ], "description": "Identifies msxsl.exe making a network connection. This may indicate adversarial activity as msxsl.exe is often leveraged by adversaries to execute malicious scripts and evade detection.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json index db96fe1bc1b50a..e82b42869e44d8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json @@ -4,7 +4,8 @@ ], "description": "Identifies when a terminal (tty) is spawned via Perl. Attackers may upgrade a simple reverse shell to a fully interactive tty after obtaining initial access to a host.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json index a5ac6cffd23763..e4c84fd3c3b83d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json @@ -7,7 +7,8 @@ "PsExec is a dual-use tool that can be used for benign or malicious activity. It's important to baseline your environment to determine the amount of noise to expect from this tool." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json index 59be6da19e93fd..3aa9ac20bba9e8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json @@ -4,7 +4,8 @@ ], "description": "Identifies when a terminal (tty) is spawned via Python. Attackers may upgrade a simple reverse shell to a fully interactive tty after obtaining initial access to a host.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json index 262313782fe332..0a1ba97bd01ea4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json @@ -7,7 +7,8 @@ "Security testing may produce events like this. Activity of this kind performed by non-engineers and ordinary users is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json index 6f9170f476d90d..7305247192f57c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json @@ -4,7 +4,8 @@ ], "description": "Identifies a PowerShell process launched by either cscript.exe or wscript.exe. Observing Windows scripting processes executing a PowerShell script, may be indicative of malicious activity.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json index 1b5fd4e1f502d3..7ff8eb9424d5fc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json @@ -4,7 +4,8 @@ ], "description": "Identifies suspicious child processes of frequently targeted Microsoft Office applications (Word, PowerPoint, Excel). These child processes are often launched during exploitation of Office applications or from documents with malicious macros.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json index f874b7e3f8e80c..e923407765f8fe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json @@ -4,7 +4,8 @@ ], "description": "Identifies suspicious child processes of Microsoft Outlook. These child processes are often associated with spear phishing activity.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json index 35206d130ea5fc..24a744ce308322 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json @@ -4,7 +4,8 @@ ], "description": "Identifies suspicious child processes of PDF reader applications. These child processes are often launched via exploitation of PDF applications or social engineering.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json index 43f1f8a5c9c616..529f2199e46dc6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json @@ -4,7 +4,8 @@ ], "description": "Identifies unusual instances of rundll32.exe making outbound network connections. This may indicate adversarial activity and may identify malicious DLLs.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json index b49d1b358cb8d3..69a25b3b24bace 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json @@ -4,7 +4,8 @@ ], "description": "Identifies network activity from unexpected system applications. This may indicate adversarial activity as these applications are often leveraged by adversaries to execute code and evade detection.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json index 2c141da80e797f..cae5d1b7e0f1fd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json @@ -4,7 +4,8 @@ ], "description": "RegSvcs.exe and RegAsm.exe are Windows command line utilities that are used to register .NET Component Object Model (COM) assemblies. Adversaries can use RegSvcs.exe and RegAsm.exe to proxy execution of code through a trusted Windows utility.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json index a9f8ee1af8bf6f..5e3e44604e9b1b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json @@ -24,7 +24,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json index 25711afbb4c66c..81d82670e794a6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Asset Visibility", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json index 8b627c48d29047..6bc14f4e5af8ab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json @@ -1,5 +1,7 @@ { - "author": ["Elastic"], + "author": [ + "Elastic" + ], "description": "Generates a detection alert for each external alert written to the configured indices. Enabling this rule allows you to immediately begin investigating external alerts in the app.", "index": [ "apm-*-transaction*", @@ -51,7 +53,9 @@ "value": "99" } ], - "tags": ["Elastic"], + "tags": [ + "Elastic" + ], "timestamp_override": "event.ingested", "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json index 27e50313c8f82d..ee434efa019d64 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json index 0bafa56c9af49b..2de24a81552548 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json @@ -25,7 +25,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json index 74b5e0d93c4417..9fe0d97ceda320 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json index 59c659117c098a..085acb9a2fb130 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Logging", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json index 10a1989ad64239..f75e4d15f1e6af 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json @@ -26,7 +26,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Data Protection", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json index 4aa0b355171fe0..68ad10977f4d3b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json @@ -25,7 +25,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json index 25b300d33cce1f..aab2deff3a266b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json @@ -25,7 +25,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json index 9ca8b7ed21acb6..abcc6f65fbc676 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json @@ -20,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json index e8343f1b7b7c63..b0615cf032386e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json @@ -27,7 +27,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Asset Visibility", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json index 8c4387e60d281d..d77533e5183ade 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json @@ -27,7 +27,10 @@ "severity": "medium", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Asset Visibility", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json index 829d87c1964c9b..026e1e549b574f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json @@ -24,7 +24,10 @@ "severity": "high", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json index 7429c69fc3174e..bd20be0924d059 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json @@ -24,7 +24,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json index 25bf7dd287d05c..2344346c8d61d7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json @@ -23,7 +23,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json index b4850e77ae7190..8a68b26abad200 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json @@ -4,7 +4,8 @@ ], "description": "Identifies unexpected processes making network connections over port 445. Windows File Sharing is typically implemented over Server Message Block (SMB), which communicates between hosts using port 445. When legitimate, these network connections are established by the kernel. Processes making 445/tcp connections may be port scanners, exploits, or suspicious user-level processes moving laterally.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json index 27e5da09452e7f..2ea75dbd758cb8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json @@ -7,7 +7,8 @@ "Telnet can be used for both benign or malicious purposes. Telnet is included by default in some Linux distributions, so its presence is not inherently suspicious. The use of Telnet to manage devices remotely has declined in recent years in favor of more secure protocols such as SSH. Telnet usage by non-automated tools or frameworks may be suspicious." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json index 0273800c18d52d..4379759608aba3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json @@ -7,7 +7,8 @@ "Telnet can be used for both benign or malicious purposes. Telnet is included by default in some Linux distributions, so its presence is not inherently suspicious. The use of Telnet to manage devices remotely has declined in recent years in favor of more secure protocols such as SSH. Telnet usage by non-automated tools or frameworks may be suspicious." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json index a842d8ef952ffd..24104439cd0ec0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json @@ -7,7 +7,8 @@ "Normal use of hping is uncommon apart from security testing and research. Use by non-security engineers is very uncommon." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json index c1ce773c2aa44b..73bf20a5a175e7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json @@ -7,7 +7,8 @@ "Normal use of Iodine is uncommon apart from security testing and research. Use by non-security engineers is very uncommon." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json index 98b262edfe6f6d..1895caf4dea812 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json @@ -7,7 +7,8 @@ "Mknod is a Linux system program. Some normal use of this program, at varying levels of frequency, may originate from scripts, automation tools, and frameworks. Usage by web servers is more likely to be suspicious." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json index 30d34f245c6d2f..ac46bcbdbc083d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json @@ -7,7 +7,8 @@ "Netcat is a dual-use tool that can be used for benign or malicious activity. Netcat is included in some Linux distributions so its presence is not necessarily suspicious. Some normal use of this program, while uncommon, may originate from scripts, automation tools, and frameworks." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json index 57f5fe57b0e0b1..2825dc28ad18f6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json @@ -7,7 +7,8 @@ "Security testing tools and frameworks may run `Nmap` in the course of security auditing. Some normal use of this command may originate from security engineers and network or server administrators. Use of nmap by ordinary users is uncommon." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json index 086492edeb8ad2..234a09e9607b9b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json @@ -7,7 +7,8 @@ "Some normal use of this command may originate from security engineers and network or server administrators, but this is usually not routine or unannounced. Use of `Nping` by non-engineers or ordinary users is uncommon." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json index 09680fcf8e996d..759622804444e3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json @@ -7,7 +7,8 @@ "Build systems, like Jenkins, may start processes in the `/tmp` directory. These can be exempted by name or by username." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json index 057d8ba9859a86..cd38aff3f21642 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json @@ -7,7 +7,8 @@ "Socat is a dual-use tool that can be used for benign or malicious activity. Some normal use of this program, at varying levels of frequency, may originate from scripts, automation tools, and frameworks. Usage by web servers is more likely to be suspicious." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json index 3dd18c8242a5e2..7fcb9f915c5608 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json @@ -7,7 +7,8 @@ "Strace is a dual-use tool that can be used for benign or malicious activity. Some normal use of this command may originate from developers or SREs engaged in debugging or system call tracing." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json index 1d15db83bb18e2..b4c2d6522fb017 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json index 6df2ed6cb34a44..f64db94fbc7b2a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json index e276166f6130b0..30e52eed861101 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json index bdfe7d25092ba6..18a72a331219ed 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json @@ -23,7 +23,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Network", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json index e3e0d5fef7b2f2..b9c6e390effd9f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json index ad21ebe065f8c4..786fdd1ac16c0f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json @@ -23,7 +23,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json index e92cf3d67d3137..06089272f0e8c1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json @@ -20,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "type": "query", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json index d5f3995fb8bcc7..a9e6d2feef813e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json index c5d8e50d3dba76..3392a1bff23b8c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json @@ -4,7 +4,8 @@ ], "description": "Detects writing executable files that will be automatically launched by Adobe on launch.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json index 5f6c006c5d177a..49b9a7501a3aa6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json index d3a66ef8d9c77f..f289e8341a0d01 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json index 7104cace1c5d98..9393f7e4ef5158 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json index c38f71d8e00a61..09aeed65f1ef0f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Okta" + "Okta", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json index 99bb07fe9660ec..229286d4e234d5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json @@ -27,7 +27,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Network", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json index 9b2478b97fb386..b62384f5bd76ad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json @@ -25,7 +25,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json index 48ed65caceda77..e76379d171bf7c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json @@ -7,7 +7,8 @@ "Security tools and device drivers may run these programs in order to load legitimate kernel modules. Use of these programs by ordinary users is uncommon." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json index b99690f78b2b4a..b9e7f941ee5dff 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json @@ -7,7 +7,8 @@ "Legitimate scheduled tasks may be created during installation of new software." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json index 94a695a97a27ab..830d2d956125bb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json @@ -27,7 +27,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Asset Visibility", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json index 24ea80e10f5e30..0cf6fcdb3875a6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json @@ -7,7 +7,8 @@ "Network monitoring or management products may have a web server component that runs shell commands as part of normal behavior." ], "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json index c3684006a49e52..59715dae441f49 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json @@ -4,7 +4,8 @@ ], "description": "Windows services typically run as SYSTEM and can be used as a privilege escalation opportunity. Malware or penetration testers may run a shell as a service to gain SYSTEM permissions.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json index 5704f6d14bfecf..7465751d5cd49b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json @@ -4,7 +4,8 @@ ], "description": "Identifies attempts to create new local users. This is sometimes done by attackers to increase access to a system or domain.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json index 74c5376100b2bc..aff6df969d90be 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json @@ -24,7 +24,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json index 3738c04346e6ec..9550eea6ca6aa7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json @@ -4,7 +4,8 @@ ], "description": "An adversary may add the setgid bit to a file or directory in order to run a file with the privileges of the owning group. An adversary can take advantage of this to either do a shell escape or exploit a vulnerability in an application with the setgid bit to get code running in a different user\u2019s context. Additionally, adversaries can use this mechanism on their own malware to make sure they're able to execute in elevated contexts in the future.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "lucene", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json index 58dcd2d671f523..343426953add6d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json @@ -4,7 +4,8 @@ ], "description": "An adversary may add the setuid bit to a file or directory in order to run a file with the privileges of the owning user. An adversary can take advantage of this to either do a shell escape or exploit a vulnerability in an application with the setuid bit to get code running in a different user\u2019s context. Additionally, adversaries can use this mechanism on their own malware to make sure they're able to execute in elevated contexts in the future.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "lucene", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json index 9850d4d908b69b..44b50c74bafe62 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json @@ -4,7 +4,8 @@ ], "description": "A sudoers file specifies the commands that users or groups can run and from which terminals. Adversaries can take advantage of these configurations to execute commands as other users or spawn processes with higher privileges.", "index": [ - "auditbeat-*" + "auditbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json index d8b59804fecdf4..50692dae3856fa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json @@ -4,7 +4,8 @@ ], "description": "Identifies User Account Control (UAC) bypass via eventvwr.exe. Attackers bypass UAC to stealthily execute code with elevated permissions.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json index bc80953d0aa619..8f938c0ceee6d2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json @@ -4,7 +4,8 @@ ], "description": "Identifies Windows programs run from unexpected parent processes. This could indicate masquerading or other strange activity on a system.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json index 7ce54b00f211cf..e271f855e44241 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json @@ -24,7 +24,10 @@ "severity": "low", "tags": [ "AWS", - "Elastic" + "Elastic", + "SecOps", + "Identity and Access", + "Continuous Monitoring" ], "threat": [ { From 63500c89c35135854fdb66c2b47b02ebeb57f29a Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 3 Aug 2020 18:41:20 -0500 Subject: [PATCH 35/74] Do not display our app URL field on read-only view (#74183) This is a "hidden" field added as metadata during rule creation. It is omitted from all other views, but in the case of an error response during rule creation, the read-only view will display this field. In lieu of any broad changes here, this simply hides that field from the (really, any) read-only view of that data. --- .../detections/components/rules/description_step/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 0b341050fa9d58..47c12d19341740 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -213,6 +213,8 @@ export const getDescriptionItem = ( } else if (field === 'ruleType') { const ruleType: RuleType = get(field, data); return buildRuleTypeDescription(label, ruleType); + } else if (field === 'kibanaSiemAppUrl') { + return []; } const description: string = get(field, data); From 64126b425e230a1dbcd6bcada6355c3bc9cbc817 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Mon, 3 Aug 2020 20:01:11 -0400 Subject: [PATCH 36/74] [Security Solution][Resolver] Handle info and change events for children (#74007) * Handle info and change events for children * Disabling tests for children search_after * Addressing comments --- .../common/endpoint/models/event.test.ts | 44 ++++-- .../common/endpoint/models/event.ts | 18 ++- .../routes/resolver/queries/children.ts | 31 ++++- .../resolver/utils/children_helper.test.ts | 4 +- .../routes/resolver/utils/children_helper.ts | 4 +- .../routes/resolver/utils/pagination.ts | 43 +++++- .../apis/index.ts | 1 + .../apis/resolver/children.ts | 129 ++++++++++++++++++ .../apis/resolver/tree.ts | 12 +- 9 files changed, 251 insertions(+), 35 deletions(-) create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts index 62f923aa6d793f..6e6e0f443015b2 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { EndpointDocGenerator } from '../generate_data'; -import { descriptiveName, isStart } from './event'; +import { descriptiveName, isProcessRunning } from './event'; import { ResolverEvent } from '../types'; describe('Generated documents', () => { @@ -42,52 +42,66 @@ describe('Generated documents', () => { }); }); - describe('Start events', () => { - it('is a start event when event.type is a string', () => { + describe('Process running events', () => { + it('is a running event when event.type is a string', () => { const event: ResolverEvent = generator.generateEvent({ eventType: 'start', }); - expect(isStart(event)).toBeTruthy(); + expect(isProcessRunning(event)).toBeTruthy(); }); - it('is a start event when event.type is an array of strings', () => { + it('is a running event when event.type is an array of strings', () => { const event: ResolverEvent = generator.generateEvent({ eventType: ['start'], }); - expect(isStart(event)).toBeTruthy(); + expect(isProcessRunning(event)).toBeTruthy(); }); - it('is a start event when event.type is an array of strings and contains start', () => { + it('is a running event when event.type is an array of strings and contains start', () => { let event: ResolverEvent = generator.generateEvent({ eventType: ['bogus', 'start', 'creation'], }); - expect(isStart(event)).toBeTruthy(); + expect(isProcessRunning(event)).toBeTruthy(); event = generator.generateEvent({ eventType: ['start', 'bogus'], }); - expect(isStart(event)).toBeTruthy(); + expect(isProcessRunning(event)).toBeTruthy(); }); - it('is not a start event when event.type is not start', () => { + it('is not a running event when event.type is only and end type', () => { const event: ResolverEvent = generator.generateEvent({ eventType: ['end'], }); - expect(isStart(event)).toBeFalsy(); + expect(isProcessRunning(event)).toBeFalsy(); }); - it('is not a start event when event.type is empty', () => { + it('is not a running event when event.type is empty', () => { const event: ResolverEvent = generator.generateEvent({ eventType: [], }); - expect(isStart(event)).toBeFalsy(); + expect(isProcessRunning(event)).toBeFalsy(); }); - it('is not a start event when event.type is bogus', () => { + it('is not a running event when event.type is bogus', () => { const event: ResolverEvent = generator.generateEvent({ eventType: ['bogus'], }); - expect(isStart(event)).toBeFalsy(); + expect(isProcessRunning(event)).toBeFalsy(); + }); + + it('is a running event when event.type contains info', () => { + const event: ResolverEvent = generator.generateEvent({ + eventType: ['info'], + }); + expect(isProcessRunning(event)).toBeTruthy(); + }); + + it('is a running event when event.type contains change', () => { + const event: ResolverEvent = generator.generateEvent({ + eventType: ['bogus', 'change'], + }); + expect(isProcessRunning(event)).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.ts index 216b5cc0285889..1168b5edb6ffd7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.ts @@ -9,16 +9,26 @@ export function isLegacyEvent(event: ResolverEvent): event is LegacyEndpointEven return (event as LegacyEndpointEvent).endgame !== undefined; } -export function isStart(event: ResolverEvent): boolean { +export function isProcessRunning(event: ResolverEvent): boolean { if (isLegacyEvent(event)) { - return event.event?.type === 'process_start' || event.event?.action === 'fork_event'; + return ( + event.event?.type === 'process_start' || + event.event?.action === 'fork_event' || + event.event?.type === 'already_running' + ); } if (Array.isArray(event.event.type)) { - return event.event.type.includes('start'); + return ( + event.event.type.includes('start') || + event.event.type.includes('change') || + event.event.type.includes('info') + ); } - return event.event.type === 'start'; + return ( + event.event.type === 'start' || event.event.type === 'change' || event.event.type === 'info' + ); } export function eventTimestamp(event: ResolverEvent): string | undefined | number { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts index d99533e23f2c27..902d287a09e428 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts @@ -22,7 +22,13 @@ export class ChildrenQuery extends ResolverQuery { } protected legacyQuery(endpointID: string, uniquePIDs: string[]): JsonObject { + const paginationFields = this.pagination.buildQueryFields('endgame.serial_event_id'); return { + collapse: { + field: 'endgame.unique_pid', + }, + size: paginationFields.size, + sort: paginationFields.sort, query: { bool: { filter: [ @@ -42,7 +48,7 @@ export class ChildrenQuery extends ResolverQuery { bool: { should: [ { - term: { 'event.type': 'process_start' }, + terms: { 'event.type': ['process_start', 'already_running'] }, }, { term: { 'event.action': 'fork_event' }, @@ -53,12 +59,30 @@ export class ChildrenQuery extends ResolverQuery { ], }, }, - ...this.pagination.buildQueryFields('endgame.serial_event_id'), }; } protected query(entityIDs: string[]): JsonObject { + const paginationFields = this.pagination.buildQueryFieldsAsInterface('event.id'); return { + /** + * Using collapse here will only return a single event per occurrence of a process.entity_id. The events are sorted + * based on timestamp in ascending order so it will be the first event that ocurred. The actual type of event that + * we receive for this query doesn't really matter (whether it is a start, info, or exec for a particular entity_id). + * All this is trying to accomplish is removing duplicate events that indicate a process existed for a node. We + * only need to know that a process existed and it's it's ancestry array and the process.entity_id fields because + * we will use it to query for the next set of descendants. + * + * The reason it is important to only receive 1 event per occurrence of a process.entity_id is it allows us to avoid + * ES 10k limit most of the time. If instead we received multiple events with the same process.entity_id that would + * reduce the maximum number of unique children processes we could retrieve in a single query. + */ + collapse: { + field: 'process.entity_id', + }, + // do not set the search_after field because collapse does not work with it + size: paginationFields.size, + sort: paginationFields.sort, query: { bool: { filter: [ @@ -93,12 +117,11 @@ export class ChildrenQuery extends ResolverQuery { term: { 'event.kind': 'event' }, }, { - term: { 'event.type': 'start' }, + terms: { 'event.type': ['start', 'info', 'change'] }, }, ], }, }, - ...this.pagination.buildQueryFields('event.id'), }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts index 01dd59b2611d96..78e4219aad75c7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts @@ -10,12 +10,12 @@ import { TreeNode, } from '../../../../../common/endpoint/generate_data'; import { ChildrenNodesHelper } from './children_helper'; -import { eventId, isStart } from '../../../../../common/endpoint/models/event'; +import { eventId, isProcessRunning } from '../../../../../common/endpoint/models/event'; function getStartEvents(events: Event[]): Event[] { const startEvents: Event[] = []; for (const event of events) { - if (isStart(event)) { + if (isProcessRunning(event)) { startEvents.push(event); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts index d3ca7a54c16d2d..ef487897e3b4ea 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts @@ -7,7 +7,7 @@ import { entityId, parentEntityId, - isStart, + isProcessRunning, getAncestryAsArray, } from '../../../../../common/endpoint/models/event'; import { @@ -99,7 +99,7 @@ export class ChildrenNodesHelper { for (const event of startEvents) { const parentID = parentEntityId(event); const entityID = entityId(event); - if (parentID && entityID && isStart(event)) { + if (parentID && entityID && isProcessRunning(event)) { // don't actually add the start event to the node, because that'll be done in // a different call const childNode = this.getOrCreateChildNode(entityID); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts index 150f56cbd70cc8..8e160fe5fc72a7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts @@ -8,11 +8,29 @@ import { ResolverEvent } from '../../../../../common/endpoint/types'; import { eventId } from '../../../../../common/endpoint/models/event'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; +type SortFields = [ + { + '@timestamp': string; + }, + { [x: string]: string } +]; + +type SearchAfterFields = [number, string]; + interface PaginationCursor { timestamp: number; eventID: string; } +/** + * Interface for defining the returned pagination information. + */ +export interface PaginationFields { + sort: SortFields; + size: number; + searchAfter?: SearchAfterFields; +} + /** * This class handles constructing pagination cursors that resolver can use to return additional events in subsequent * queries. It also constructs an aggregation query to determine the totals for other queries. This class should be used @@ -100,6 +118,22 @@ export class PaginationBuilder { return new PaginationBuilder(limit); } + /** + * Helper for creates an object for adding the pagination fields to a query + * + * @param tiebreaker a unique field to use as the tiebreaker for the search_after + * @returns an object containing the pagination information + */ + buildQueryFieldsAsInterface(tiebreaker: string): PaginationFields { + const sort: SortFields = [{ '@timestamp': 'asc' }, { [tiebreaker]: 'asc' }]; + let searchAfter: SearchAfterFields | undefined; + if (this.timestamp && this.eventID) { + searchAfter = [this.timestamp, this.eventID]; + } + + return { sort, size: this.size, searchAfter }; + } + /** * Creates an object for adding the pagination fields to a query * @@ -108,10 +142,11 @@ export class PaginationBuilder { */ buildQueryFields(tiebreaker: string): JsonObject { const fields: JsonObject = {}; - fields.sort = [{ '@timestamp': 'asc' }, { [tiebreaker]: 'asc' }]; - fields.size = this.size; - if (this.timestamp && this.eventID) { - fields.search_after = [this.timestamp, this.eventID] as Array; + const pagination = this.buildQueryFieldsAsInterface(tiebreaker); + fields.sort = pagination.sort; + fields.size = pagination.size; + if (pagination.searchAfter) { + fields.search_after = pagination.searchAfter; } return fields; } diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index 9cdef1c938890c..90db224f31839d 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -28,6 +28,7 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider }); loadTestFile(require.resolve('./resolver/entity_id')); loadTestFile(require.resolve('./resolver/tree')); + loadTestFile(require.resolve('./resolver/children')); loadTestFile(require.resolve('./metadata')); loadTestFile(require.resolve('./policy')); loadTestFile(require.resolve('./artifacts')); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts new file mode 100644 index 00000000000000..cde1a3616b620a --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SearchResponse } from 'elasticsearch'; +import { entityId } from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; +import { PaginationBuilder } from '../../../../plugins/security_solution/server/endpoint/routes/resolver/utils/pagination'; +import { ChildrenQuery } from '../../../../plugins/security_solution/server/endpoint/routes/resolver/queries/children'; +import { + ResolverTree, + ResolverEvent, +} from '../../../../plugins/security_solution/common/endpoint/types'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { + Event, + EndpointDocGenerator, +} from '../../../../plugins/security_solution/common/endpoint/generate_data'; +import { InsertedEvents } from '../../services/resolver'; + +export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const resolver = getService('resolverGenerator'); + const generator = new EndpointDocGenerator('resolver'); + const es = getService('es'); + + describe('Resolver children edge cases', () => { + describe('info and exec children', () => { + let origin: Event; + let infoEvent: Event; + let startEvent: Event; + let execEvent: Event; + let genData: InsertedEvents; + + before(async () => { + // Construct the following tree: + // Origin -> infoEvent -> startEvent -> execEvent + origin = generator.generateEvent(); + infoEvent = generator.generateEvent({ + parentEntityID: origin.process.entity_id, + ancestry: [origin.process.entity_id], + eventType: ['info'], + }); + + startEvent = generator.generateEvent({ + parentEntityID: infoEvent.process.entity_id, + ancestry: [infoEvent.process.entity_id, origin.process.entity_id], + eventType: ['start'], + }); + + execEvent = generator.generateEvent({ + parentEntityID: startEvent.process.entity_id, + ancestry: [startEvent.process.entity_id, infoEvent.process.entity_id], + eventType: ['change'], + }); + genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); + }); + + after(async () => { + await resolver.deleteData(genData); + }); + + it('finds all the children of the origin', async () => { + const { body }: { body: ResolverTree } = await supertest + .get(`/api/endpoint/resolver/${origin.process.entity_id}?children=100`) + .expect(200); + expect(body.children.childNodes.length).to.be(3); + expect(body.children.childNodes[0].entityID).to.be(infoEvent.process.entity_id); + expect(body.children.childNodes[1].entityID).to.be(startEvent.process.entity_id); + expect(body.children.childNodes[2].entityID).to.be(execEvent.process.entity_id); + }); + }); + + describe('duplicate process running events', () => { + let origin: Event; + let startEvent: Event; + let infoEvent: Event; + let execEvent: Event; + let genData: InsertedEvents; + + before(async () => { + // Construct the following tree: + // Origin -> (infoEvent, startEvent, execEvent are all for the same node) + origin = generator.generateEvent(); + startEvent = generator.generateEvent({ + parentEntityID: origin.process.entity_id, + ancestry: [origin.process.entity_id], + eventType: ['start'], + }); + + infoEvent = generator.generateEvent({ + parentEntityID: origin.process.entity_id, + ancestry: [origin.process.entity_id], + entityID: startEvent.process.entity_id, + eventType: ['info'], + }); + + execEvent = generator.generateEvent({ + parentEntityID: origin.process.entity_id, + ancestry: [origin.process.entity_id], + eventType: ['change'], + entityID: startEvent.process.entity_id, + }); + genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); + }); + + after(async () => { + await resolver.deleteData(genData); + }); + + it('only retrieves the start event for the child node', async () => { + const childrenQuery = new ChildrenQuery( + PaginationBuilder.createBuilder(100), + eventsIndexPattern + ); + // [1] here gets the body portion of the array + const [, query] = childrenQuery.buildMSearch(origin.process.entity_id); + const { body } = await es.search>({ body: query }); + expect(body.hits.hits.length).to.be(1); + + const event = body.hits.hits[0]._source; + expect(entityId(event)).to.be(startEvent.process.entity_id); + expect(event.event?.type).to.eql(['start']); + }); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts index 3527e7e575c996..758e26fd5eeced 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts @@ -566,7 +566,8 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ).to.eql(93932); }); - it('returns no values when there is no more data', async () => { + // The children api does not support pagination currently + it.skip('returns no values when there is no more data', async () => { const { body } = await supertest // after is set to the document id of the last event so there shouldn't be any more after it .get( @@ -577,7 +578,8 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC expect(body.nextChild).to.eql(null); }); - it('returns the first page of information when the cursor is invalid', async () => { + // The children api does not support pagination currently + it.skip('returns the first page of information when the cursor is invalid', async () => { const { body }: { body: ResolverChildren } = await supertest .get( `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&afterChild=blah` @@ -639,7 +641,8 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC expect(body.nextChild).to.not.eql(null); }); - it('paginates the children', async () => { + // children api does not support pagination currently + it.skip('paginates the children', async () => { // this gets a node should have 3 children which were created in succession so that the timestamps // are ordered correctly to be retrieved in a single call const distantChildEntityID = Array.from(tree.childrenLevels[0].values())[0].id; @@ -668,7 +671,8 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC expect(body.nextChild).to.be(null); }); - it('gets all children in two queries', async () => { + // children api does not support pagination currently + it.skip('gets all children in two queries', async () => { // should get all the children of the origin let { body }: { body: ResolverChildren } = await supertest .get(`/api/endpoint/resolver/${tree.origin.id}/children?children=3`) From 4031f557012a9410fe2aa06add3e60b27b608864 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Mon, 3 Aug 2020 18:25:12 -0600 Subject: [PATCH 37/74] [Security Solution] Fixes scroll issues related to the sticky header (#74062) ## [Security Solution] Fixes scroll issues related to the sticky header Fixes scrolling issues related to the sticky header. The Security solution hid the app navigation links (e.g. `Overview`, `Detections`, `Hosts` ...) in the sticky header when the page was scrolled (to maximize the available vertical space), but in recent `7.9` BCs, sometimes this [created issues while scrolling](https://github.com/elastic/kibana/issues/73882). With the introduction of Full Screen mode in Timeline-based views, it's no longer necessary to hide the app navigation links while scrolling. (The navigation links are hidden when Timeline-based views are placed into full screen mode.) Fixes: https://github.com/elastic/kibana/issues/73882 ## Desk testing Desk tested in: - Chrome `84.0.4147.105` - Firefox `79.0` - Safari `13.1.2` --- .../public/app/home/index.tsx | 49 ++++------- .../components/alerts_viewer/alerts_table.tsx | 3 - .../common/components/alerts_viewer/index.tsx | 22 +---- .../events_viewer/events_viewer.tsx | 24 +++-- .../common/components/events_viewer/index.tsx | 70 ++++++++------- .../filters_global.test.tsx.snap | 23 +++-- .../filters_global/filters_global.test.tsx | 88 +++---------------- .../filters_global/filters_global.tsx | 60 ++++--------- .../common/components/header_global/index.tsx | 43 ++++++--- .../common/components/inspect/index.tsx | 1 + .../public/common/components/page/index.tsx | 28 +++++- .../common/components/wrapper_page/index.tsx | 23 +++-- .../containers/use_full_screen/index.tsx | 19 +++- .../common/hooks/use_global_header_portal.tsx | 19 ++++ .../components/alerts_table/index.tsx | 3 - .../detection_engine/detection_engine.tsx | 31 +------ .../detection_engine/rules/details/index.tsx | 32 +------ .../public/hosts/pages/details/index.tsx | 10 +-- .../public/hosts/pages/hosts.tsx | 10 +-- .../navigation/events_query_tab_body.tsx | 23 +---- .../public/network/pages/ip_details/index.tsx | 7 +- .../public/network/pages/network.tsx | 10 +-- .../public/overview/pages/overview.tsx | 7 +- .../flyout/__snapshots__/index.test.tsx.snap | 1 - .../components/flyout/index.test.tsx | 16 ++-- .../timelines/components/flyout/index.tsx | 10 +-- .../pane/__snapshots__/index.test.tsx.snap | 1 - .../components/flyout/pane/index.test.tsx | 29 +----- .../components/flyout/pane/index.tsx | 1 - .../components/graph_overlay/index.tsx | 8 +- .../components/timeline/body/helpers.ts | 35 -------- .../components/timeline/body/index.test.tsx | 2 - .../components/timeline/body/index.tsx | 10 +-- .../timeline/body/stateful_body.tsx | 4 - .../components/timeline/footer/index.tsx | 2 +- 35 files changed, 264 insertions(+), 460 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 41b9252c67b8a6..f230566fa08e6a 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -7,7 +7,6 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { useThrottledResizeObserver } from '../../common/components/utils'; import { DragDropContextWrapper } from '../../common/components/drag_and_drop/drag_drop_context_wrapper'; import { Flyout } from '../../timelines/components/flyout'; import { HeaderGlobal } from '../../common/components/header_global'; @@ -19,43 +18,29 @@ import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline'; import { navTabs } from './home_navigations'; import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index'; -const WrappedByAutoSizer = styled.div` +const SecuritySolutionAppWrapper = styled.div` + display: flex; + flex-direction: column; height: 100%; + width: 100%; `; -WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; +SecuritySolutionAppWrapper.displayName = 'SecuritySolutionAppWrapper'; const Main = styled.main` - height: 100%; + position: relative; + overflow: auto; + flex: 1; `; + Main.displayName = 'Main'; const usersViewing = ['elastic']; // TODO: get the users viewing this timeline from Elasticsearch (persistance) -/** the global Kibana navigation at the top of every page */ -export const globalHeaderHeightPx = 48; - -const calculateFlyoutHeight = ({ - globalHeaderSize, - windowHeight, -}: { - globalHeaderSize: number; - windowHeight: number; -}): number => Math.max(0, windowHeight - globalHeaderSize); - interface HomePageProps { children: React.ReactNode; } -export const HomePage: React.FC = ({ children }) => { - const { ref: measureRef, height: windowHeight = 0 } = useThrottledResizeObserver(); - const flyoutHeight = useMemo( - () => - calculateFlyoutHeight({ - globalHeaderSize: globalHeaderHeightPx, - windowHeight, - }), - [windowHeight] - ); +const HomePageComponent: React.FC = ({ children }) => { const { signalIndexExists, signalIndexName } = useSignalIndex(); const indexToAdd = useMemo(() => { @@ -69,7 +54,7 @@ export const HomePage: React.FC = ({ children }) => { const { browserFields, indexPattern, indicesExist } = useWithSource('default', indexToAdd); return ( - +
@@ -78,11 +63,7 @@ export const HomePage: React.FC = ({ children }) => { {indicesExist && showTimeline && ( <> - + )} @@ -91,8 +72,10 @@ export const HomePage: React.FC = ({ children }) => {
-
+ ); }; -HomePage.displayName = 'HomePage'; +HomePageComponent.displayName = 'HomePage'; + +export const HomePage = React.memo(HomePageComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index e30560f6c81477..841a1ef09ede6c 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -58,7 +58,6 @@ const defaultAlertsFilters: Filter[] = [ interface Props { timelineId: TimelineIdLiteral; endDate: string; - eventsViewerBodyHeight?: number; startDate: string; pageFilters?: Filter[]; } @@ -66,7 +65,6 @@ interface Props { const AlertsTableComponent: React.FC = ({ timelineId, endDate, - eventsViewerBodyHeight, startDate, pageFilters = [], }) => { @@ -93,7 +91,6 @@ const AlertsTableComponent: React.FC = ({ pageFilters={alertsFilter} defaultModel={alertsDefaultModel} end={endDate} - height={eventsViewerBodyHeight} id={timelineId} start={startDate} /> diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx index 832b14f00159a6..633135d63ac33a 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx @@ -5,17 +5,9 @@ */ import React, { useEffect, useCallback, useMemo } from 'react'; import numeral from '@elastic/numeral'; -import { useWindowSize } from 'react-use'; -import { globalHeaderHeightPx } from '../../../app/home'; -import { DEFAULT_NUMBER_FORMAT, FILTERS_GLOBAL_HEIGHT } from '../../../../common/constants'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; import { useFullScreen } from '../../containers/use_full_screen'; -import { EVENTS_VIEWER_HEADER_HEIGHT } from '../events_viewer/events_viewer'; -import { - getEventsViewerBodyHeight, - MIN_EVENTS_VIEWER_BODY_HEIGHT, -} from '../../../timelines/components/timeline/body/helpers'; -import { footerHeight } from '../../../timelines/components/timeline/footer'; import { AlertsComponentsProps } from './types'; import { AlertsTable } from './alerts_table'; @@ -45,7 +37,6 @@ export const AlertsView = ({ // eslint-disable-next-line react-hooks/exhaustive-deps [] ); - const { height: windowHeight } = useWindowSize(); const { globalFullScreen } = useFullScreen(); const alertsHistogramConfigs: MatrixHisrogramConfigs = useMemo( () => ({ @@ -79,17 +70,6 @@ export const AlertsView = ({ diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index e836e2e20432aa..436386077e7254 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { getOr, isEmpty, union } from 'lodash/fp'; import React, { useEffect, useMemo, useState } from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; import { BrowserFields, DocValueFields } from '../../containers/source'; @@ -50,18 +50,18 @@ const TitleText = styled.span` margin-right: 12px; `; -const DEFAULT_EVENTS_VIEWER_HEIGHT = 500; - const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>` + display: flex; + flex-direction: column; + ${({ $isFullScreen }) => $isFullScreen && - css` + ` border: 0; box-shadow: none; padding-top: 0; padding-bottom: 0; - `} - max-width: 100%; + `} `; const TitleFlexGroup = styled(EuiFlexGroup)` @@ -70,7 +70,10 @@ const TitleFlexGroup = styled(EuiFlexGroup)` const EventsContainerLoading = styled.div` width: 100%; - overflow: auto; + overflow: hidden; + flex: 1; + display: flex; + flex-direction: column; `; /** @@ -78,9 +81,7 @@ const EventsContainerLoading = styled.div` * from being unmounted, to preserve the state of the component */ const HeaderFilterGroupWrapper = styled.header<{ show: boolean }>` - ${({ show }) => css` - ${show ? '' : 'visibility: hidden;'}; - `} + ${({ show }) => (show ? '' : 'visibility: hidden;')} `; interface Props { @@ -119,7 +120,6 @@ const EventsViewerComponent: React.FC = ({ end, filters, headerFilterGroup, - height = DEFAULT_EVENTS_VIEWER_HEIGHT, id, indexPattern, isLive, @@ -277,7 +277,6 @@ const EventsViewerComponent: React.FC = ({ docValueFields={docValueFields} id={id} isEventViewer={true} - height={height} sort={sort} toggleColumn={toggleColumn} /> @@ -326,7 +325,6 @@ export const EventsViewer = React.memo( prevProps.end === nextProps.end && deepEqual(prevProps.filters, nextProps.filters) && prevProps.headerFilterGroup === nextProps.headerFilterGroup && - prevProps.height === nextProps.height && prevProps.id === nextProps.id && deepEqual(prevProps.indexPattern, nextProps.indexPattern) && prevProps.isLive === nextProps.isLive && diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index c402116ee27146..1563eab6039a61 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useMemo, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; +import styled from 'styled-components'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { inputsModel, inputsSelectors, State } from '../../store'; @@ -23,12 +24,20 @@ import { useUiSetting } from '../../lib/kibana'; import { EventsViewer } from './events_viewer'; import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns'; import { InspectButtonContainer } from '../inspect'; +import { useFullScreen } from '../../containers/use_full_screen'; + +const DEFAULT_EVENTS_VIEWER_HEIGHT = 652; + +const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` + height: ${({ $isFullScreen }) => ($isFullScreen ? '100%' : `${DEFAULT_EVENTS_VIEWER_HEIGHT}px`)}; + display: flex; + width: 100%; +`; export interface OwnProps { defaultIndices?: string[]; defaultModel: SubsetTimelineModel; end: string; - height?: number; id: string; start: string; headerFilterGroup?: React.ReactNode; @@ -49,7 +58,6 @@ const StatefulEventsViewerComponent: React.FC = ({ excludedRowRendererIds, filters, headerFilterGroup, - height, id, isLive, itemsPerPage, @@ -74,6 +82,8 @@ const StatefulEventsViewerComponent: React.FC = ({ 'events_viewer' ); + const { globalFullScreen } = useFullScreen(); + useEffect(() => { if (createTimeline != null) { createTimeline({ @@ -121,33 +131,34 @@ const StatefulEventsViewerComponent: React.FC = ({ const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); return ( - - - + + + + + ); }; @@ -219,7 +230,6 @@ export const StatefulEventsViewer = connector( prevProps.deletedEventIds === nextProps.deletedEventIds && prevProps.end === nextProps.end && deepEqual(prevProps.filters, nextProps.filters) && - prevProps.height === nextProps.height && prevProps.isLive === nextProps.isLive && prevProps.itemsPerPage === nextProps.itemsPerPage && deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap index 35fe74abff2842..994e98d8619a18 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap @@ -1,13 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`rendering renders correctly 1`] = ` -} > - - + + +

+ Additional filters here. +

+
+
+ `; diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.test.tsx b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.test.tsx index 9fda60b1f289d6..d9032092744a18 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.test.tsx @@ -6,7 +6,6 @@ import { mount, ReactWrapper, shallow } from 'enzyme'; import React from 'react'; -import { StickyContainer } from 'react-sticky'; import '../../mock/match_media'; import { FiltersGlobal } from './filters_global'; @@ -15,7 +14,7 @@ import { TestProviders } from '../../mock/test_providers'; describe('rendering', () => { test('renders correctly', () => { const wrapper = shallow( - +

{'Additional filters here.'}

); @@ -23,101 +22,40 @@ describe('rendering', () => { expect(wrapper).toMatchSnapshot(); }); - describe('full screen mode', () => { + describe('when show is true (the default)', () => { let wrapper: ReactWrapper; beforeEach(() => { wrapper = mount( - - -

{'Filter content'}

-
-
+ +

{'Filter content'}

+
); }); - test('it does NOT render the sticky container', () => { - expect(wrapper.find('[data-test-subj="sticky-filters-global-container"]').exists()).toBe( - false - ); - }); - - test('it renders the non-sticky container', () => { - expect(wrapper.find('[data-test-subj="non-sticky-global-container"]').exists()).toBe(true); - }); - test('it does NOT render the container with a `display: none` style when `show` is true (the default)', () => { expect( - wrapper.find('[data-test-subj="non-sticky-global-container"]').first() - ).not.toHaveStyleRule('display', 'none'); - }); - }); - - describe('non-full screen mode', () => { - let wrapper: ReactWrapper; - - beforeEach(() => { - wrapper = mount( - - - -

{'Filter content'}

-
-
-
- ); - }); - - test('it renders the sticky container', () => { - expect(wrapper.find('[data-test-subj="sticky-filters-global-container"]').exists()).toBe( - true - ); - }); - - test('it does NOT render the non-sticky container', () => { - expect(wrapper.find('[data-test-subj="non-sticky-global-container"]').exists()).toBe(false); - }); - - test('it does NOT render the container with a `display: none` style when `show` is true (the default)', () => { - expect( - wrapper.find('[data-test-subj="sticky-filters-global-container"]').first() + wrapper.find('[data-test-subj="filters-global-container"]').first() ).not.toHaveStyleRule('display', 'none'); }); }); describe('when show is false', () => { - test('in full screen mode it renders the container with a `display: none` style', () => { + test('it renders the container with a `display: none` style', () => { const wrapper = mount( - - -

{'Filter content'}

-
-
+ +

{'Filter content'}

+
); - expect( - wrapper.find('[data-test-subj="non-sticky-global-container"]').first() - ).toHaveStyleRule('display', 'none'); - }); - - test('in non-full screen mode it renders the container with a `display: none` style', () => { - const wrapper = mount( - - - -

{'Filter content'}

-
-
-
+ expect(wrapper.find('[data-test-subj="filters-global-container"]').first()).toHaveStyleRule( + 'display', + 'none' ); - - expect( - wrapper.find('[data-test-subj="sticky-filters-global-container"]').first() - ).toHaveStyleRule('display', 'none'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx index 80e7209492fa5c..c324b812a9ec23 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx @@ -4,38 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import React from 'react'; -import { Sticky } from 'react-sticky'; import styled, { css } from 'styled-components'; +import { InPortal } from 'react-reverse-portal'; -import { FILTERS_GLOBAL_HEIGHT } from '../../../../common/constants'; +import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal'; import { gutterTimeline } from '../../lib/helpers'; -const offsetChrome = 49; - -const disableSticky = `screen and (max-width: ${euiLightVars.euiBreakpoints.s})`; -const disableStickyMq = window.matchMedia(disableSticky); - -const Wrapper = styled.aside<{ isSticky?: boolean }>` - height: ${FILTERS_GLOBAL_HEIGHT}px; +const Wrapper = styled.aside` position: relative; z-index: ${({ theme }) => theme.eui.euiZNavigation}; background: ${({ theme }) => theme.eui.euiColorEmptyShade}; border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; - padding: ${({ theme }) => theme.eui.paddingSizes.m} ${gutterTimeline} ${({ theme }) => - theme.eui.paddingSizes.m} ${({ theme }) => theme.eui.paddingSizes.l}; - - ${({ isSticky }) => - isSticky && - css` - top: ${offsetChrome}px !important; - `} - - @media only ${disableSticky} { - position: static !important; - z-index: ${({ theme }) => theme.eui.euiZContent} !important; - } + padding: ${({ theme }) => theme.eui.paddingSizes.m} ${gutterTimeline} + ${({ theme }) => theme.eui.paddingSizes.m} ${({ theme }) => theme.eui.paddingSizes.l}; `; Wrapper.displayName = 'Wrapper'; @@ -47,33 +29,21 @@ const FiltersGlobalContainer = styled.header<{ show: boolean }>` FiltersGlobalContainer.displayName = 'FiltersGlobalContainer'; -const NO_STYLE: React.CSSProperties = {}; - export interface FiltersGlobalProps { children: React.ReactNode; - globalFullScreen: boolean; show?: boolean; } -export const FiltersGlobal = React.memo( - ({ children, globalFullScreen, show = true }) => - globalFullScreen ? ( - - - {children} - +export const FiltersGlobal = React.memo(({ children, show = true }) => { + const { globalHeaderPortalNode } = useGlobalHeaderPortal(); + + return ( + + + {children} - ) : ( - - {({ style, isSticky }) => ( - - - {children} - - - )} - - ) -); + + ); +}); FiltersGlobal.displayName = 'FiltersGlobal'; diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx index a1e7293ce974b1..fbc3d62768d003 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx @@ -7,29 +7,31 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { pickBy } from 'lodash/fp'; import React, { useCallback } from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; +import { OutPortal } from 'react-reverse-portal'; import { gutterTimeline } from '../../lib/helpers'; import { navTabs } from '../../../app/home/home_navigations'; +import { useFullScreen } from '../../containers/use_full_screen'; import { SecurityPageName } from '../../../app/types'; import { getAppOverviewUrl } from '../link_to'; import { MlPopover } from '../ml_popover/ml_popover'; import { SiemNavigation } from '../navigation'; import * as i18n from './translations'; import { useWithSource } from '../../containers/source'; -import { useFullScreen } from '../../containers/use_full_screen'; import { useGetUrlSearch } from '../navigation/use_get_url_search'; import { useKibana } from '../../lib/kibana'; import { APP_ID, ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants'; +import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal'; import { LinkAnchor } from '../links'; -const Wrapper = styled.header<{ show: boolean }>` - ${({ show, theme }) => css` +const Wrapper = styled.header<{ $globalFullScreen: boolean }>` + ${({ $globalFullScreen, theme }) => ` background: ${theme.eui.euiColorEmptyShade}; border-bottom: ${theme.eui.euiBorderThin}; - padding: ${theme.eui.paddingSizes.m} ${gutterTimeline} ${theme.eui.paddingSizes.m} - ${theme.eui.paddingSizes.l}; - ${show ? '' : 'display: none;'}; + padding-top: ${$globalFullScreen ? theme.eui.paddingSizes.s : theme.eui.paddingSizes.m}; + width: 100%; + z-index: ${theme.eui.euiZNavigation}; `} `; Wrapper.displayName = 'Wrapper'; @@ -39,11 +41,24 @@ const FlexItem = styled(EuiFlexItem)` `; FlexItem.displayName = 'FlexItem'; +const FlexGroup = styled(EuiFlexGroup)<{ $globalFullScreen: boolean }>` + ${({ $globalFullScreen, theme }) => ` + border-bottom: ${theme.eui.euiBorderThin}; + margin-bottom: 1px; + padding-bottom: 4px; + padding-left: ${theme.eui.paddingSizes.l}; + padding-right: ${gutterTimeline}; + ${$globalFullScreen ? 'display: none;' : ''} + `} +`; +FlexGroup.displayName = 'FlexGroup'; + interface HeaderGlobalProps { hideDetectionEngine?: boolean; } export const HeaderGlobal = React.memo(({ hideDetectionEngine = false }) => { const { indicesExist } = useWithSource(); + const { globalHeaderPortalNode } = useGlobalHeaderPortal(); const { globalFullScreen } = useFullScreen(); const search = useGetUrlSearch(navTabs.overview); const { navigateToApp } = useKibana().services.application; @@ -56,8 +71,13 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine ); return ( - - + + <> @@ -100,7 +120,10 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine - + +
+ +
); }); diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx index 3dc120b3d874dc..435f3f6e349d06 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx @@ -20,6 +20,7 @@ import * as i18n from './translations'; export const BUTTON_CLASS = 'inspectButtonComponent'; export const InspectButtonContainer = styled.div<{ show?: boolean }>` + width: 100%; display: flex; flex-grow: 1; diff --git a/x-pack/plugins/security_solution/public/common/components/page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page/index.tsx index 8bf0690bfd0adf..6c49ce7453ce90 100644 --- a/x-pack/plugins/security_solution/public/common/components/page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page/index.tsx @@ -62,15 +62,39 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar z-index: 9950; } - /** applies a "toggled" button style to the Full Screen button */ + /* applies a "toggled" button style to the Full Screen button */ .${FULL_SCREEN_TOGGLED_CLASS_NAME} { ${({ theme }) => `background-color: ${theme.eui.euiColorPrimary} !important`}; } - .${SCROLLING_DISABLED_CLASS_NAME} body { + body { overflow-y: hidden; } + #kibana-body { + height: 100%; + + > .content { + height: 100%; + + > .app-wrapper { + height: 100%; + + > .app-wrapper-panel { + height: 100%; + + > .application { + height: 100%; + + > div { + height: 100%; + } + } + } + } + } + } + .${SCROLLING_DISABLED_CLASS_NAME} #kibana-body { overflow-y: hidden; } diff --git a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx index 03f9b43678003a..373c1f7aaec757 100644 --- a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx @@ -12,12 +12,9 @@ import { useFullScreen } from '../../containers/use_full_screen'; import { gutterTimeline } from '../../lib/helpers'; import { AppGlobalStyle } from '../page/index'; -const Wrapper = styled.div<{ noPadding?: boolean }>` - padding: ${(props) => - props.noPadding - ? '0' - : `${props.theme.eui.paddingSizes.l} ${gutterTimeline} ${props.theme.eui.paddingSizes.l} - ${props.theme.eui.paddingSizes.l}`}; +const Wrapper = styled.div` + padding: ${({ theme }) => + `${theme.eui.paddingSizes.l} ${gutterTimeline} ${theme.eui.paddingSizes.l} ${theme.eui.paddingSizes.l}`}; &.siemWrapperPage--restrictWidthDefault, &.siemWrapperPage--restrictWidthCustom { box-sizing: content-box; @@ -27,6 +24,14 @@ const Wrapper = styled.div<{ noPadding?: boolean }>` &.siemWrapperPage--restrictWidthDefault { max-width: 1000px; } + + &.siemWrapperPage--fullHeight { + height: 100%; + } + + &.siemWrapperPage--noPadding { + padding: 0; + } `; Wrapper.displayName = 'Wrapper'; @@ -46,13 +51,15 @@ const WrapperPageComponent: React.FC = ({ style, noPadding, }) => { - const { setGlobalFullScreen } = useFullScreen(); + const { globalFullScreen, setGlobalFullScreen } = useFullScreen(); useEffect(() => { setGlobalFullScreen(false); // exit full screen mode on page load }, [setGlobalFullScreen]); const classes = classNames(className, { siemWrapperPage: true, + 'siemWrapperPage--noPadding': noPadding, + 'siemWrapperPage--fullHeight': globalFullScreen, 'siemWrapperPage--restrictWidthDefault': restrictWidth && typeof restrictWidth === 'boolean' && restrictWidth === true, 'siemWrapperPage--restrictWidthCustom': restrictWidth && typeof restrictWidth !== 'boolean', @@ -66,7 +73,7 @@ const WrapperPageComponent: React.FC = ({ } return ( - + {children} diff --git a/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx index aa0d90a2160350..32591fb032436d 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx @@ -11,6 +11,22 @@ import { SCROLLING_DISABLED_CLASS_NAME } from '../../../../common/constants'; import { inputsSelectors } from '../../store'; import { inputsActions } from '../../store/actions'; +export const resetScroll = () => { + setTimeout(() => { + window.scrollTo(0, 0); + + const kibanaBody = document.querySelector('#kibana-body'); + if (kibanaBody != null) { + kibanaBody.scrollTop = 0; + } + + const pageContainer = document.querySelector('[data-test-subj="pageContainer"]'); + if (pageContainer != null) { + pageContainer.scrollTop = 0; + } + }, 0); +}; + export const useFullScreen = () => { const dispatch = useDispatch(); const globalFullScreen = useSelector(inputsSelectors.globalFullScreenSelector) ?? false; @@ -20,9 +36,10 @@ export const useFullScreen = () => { (fullScreen: boolean) => { if (fullScreen) { document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME); + resetScroll(); } else { document.body.classList.remove(SCROLLING_DISABLED_CLASS_NAME); - setTimeout(() => window.scrollTo(0, 0), 0); + resetScroll(); } dispatch(inputsActions.setFullScreen({ id: 'global', fullScreen })); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx new file mode 100644 index 00000000000000..546d2615fe6ae7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; +import { createPortalNode } from 'react-reverse-portal'; + +/** + * A singleton portal for rendering content in the global header + */ +const globalHeaderPortalNodeSingleton = createPortalNode(); + +export const useGlobalHeaderPortal = () => { + const [globalHeaderPortalNode] = useState(globalHeaderPortalNodeSingleton); + + return { globalHeaderPortalNode }; +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index ab95e433d92f3e..d93bad29f33487 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -61,7 +61,6 @@ interface OwnProps { timelineId: TimelineIdLiteral; canUserCRUD: boolean; defaultFilters?: Filter[]; - eventsViewerBodyHeight?: number; hasIndexWrite: boolean; from: string; loading: boolean; @@ -88,7 +87,6 @@ export const AlertsTableComponent: React.FC = ({ clearEventsLoading, clearSelected, defaultFilters, - eventsViewerBodyHeight, from, globalFilters, globalQuery, @@ -451,7 +449,6 @@ export const AlertsTableComponent: React.FC = ({ defaultModel={alertsDefaultModel} end={to} headerFilterGroup={headerFilterGroup} - height={eventsViewerBodyHeight} id={timelineId} start={from} utilityBar={utilityBarCallback} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index c114e4519df10a..d76da592e1c811 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -7,12 +7,9 @@ import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useState } from 'react'; -import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; -import { useWindowSize } from 'react-use'; import { useHistory } from 'react-router-dom'; -import { globalHeaderHeightPx } from '../../../app/home'; import { SecurityPageName } from '../../../app/types'; import { TimelineId } from '../../../../common/types/timeline'; import { useGlobalTime } from '../../../common/containers/use_global_time'; @@ -34,7 +31,6 @@ import { NoWriteAlertsCallOut } from '../../components/no_write_alerts_callout'; import { AlertsHistogramPanel } from '../../components/alerts_histogram_panel'; import { alertsHistogramOptions } from '../../components/alerts_histogram_panel/config'; import { useUserInfo } from '../../components/user_info'; -import { EVENTS_VIEWER_HEADER_HEIGHT } from '../../../common/components/events_viewer/events_viewer'; import { OverviewEmpty } from '../../../overview/components/overview_empty'; import { DetectionEngineNoIndex } from './detection_engine_no_index'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; @@ -43,14 +39,8 @@ import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unau import * as i18n from './translations'; import { LinkButton } from '../../../common/components/links'; import { useFormatUrl } from '../../../common/components/link_to'; -import { FILTERS_GLOBAL_HEIGHT } from '../../../../common/constants'; import { useFullScreen } from '../../../common/containers/use_full_screen'; import { Display } from '../../../hosts/pages/display'; -import { - getEventsViewerBodyHeight, - MIN_EVENTS_VIEWER_BODY_HEIGHT, -} from '../../../timelines/components/timeline/body/helpers'; -import { footerHeight } from '../../../timelines/components/timeline/footer'; import { showGlobalFilters } from '../../../timelines/components/timeline/helpers'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; @@ -64,7 +54,6 @@ export const DetectionEnginePageComponent: React.FC = ({ setAbsoluteRangeDatePicker, }) => { const { to, from, deleteQuery, setQuery } = useGlobalTime(); - const { height: windowHeight } = useWindowSize(); const { globalFullScreen } = useFullScreen(); const { loading: userInfoLoading, @@ -157,12 +146,9 @@ export const DetectionEnginePageComponent: React.FC = ({ {hasEncryptionKey != null && !hasEncryptionKey && } {hasIndexWrite != null && !hasIndexWrite && } {indicesExist ? ( - + <> - + @@ -210,17 +196,6 @@ export const DetectionEnginePageComponent: React.FC = ({ loading={loading} hasIndexWrite={hasIndexWrite ?? false} canUserCRUD={(canUserCRUD ?? false) && (hasEncryptionKey ?? false)} - eventsViewerBodyHeight={ - globalFullScreen - ? getEventsViewerBodyHeight({ - footerHeight, - headerHeight: EVENTS_VIEWER_HEADER_HEIGHT, - kibanaChromeHeight: globalHeaderHeightPx, - otherContentHeight: FILTERS_GLOBAL_HEIGHT, - windowHeight, - }) - : MIN_EVENTS_VIEWER_BODY_HEIGHT - } from={from} defaultFilters={alertsTableDefaultFilters} showBuildingBlockAlerts={showBuildingBlockAlerts} @@ -229,7 +204,7 @@ export const DetectionEnginePageComponent: React.FC = ({ to={to} /> - + ) : ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 789469e981fb45..4327ef96c93a7e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -21,11 +21,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { noop } from 'lodash/fp'; import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useParams, useHistory } from 'react-router-dom'; -import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; -import { useWindowSize } from 'react-use'; -import { globalHeaderHeightPx } from '../../../../../app/home'; import { TimelineId } from '../../../../../../common/types/timeline'; import { UpdateDateRange } from '../../../../../common/components/charts/common'; import { FiltersGlobal } from '../../../../../common/components/filters_global'; @@ -66,7 +63,6 @@ import * as ruleI18n from '../translations'; import * as i18n from './translations'; import { useGlobalTime } from '../../../../../common/containers/use_global_time'; import { alertsHistogramOptions } from '../../../../components/alerts_histogram_panel/config'; -import { EVENTS_VIEWER_HEADER_HEIGHT } from '../../../../../common/components/events_viewer/events_viewer'; import { inputsSelectors } from '../../../../../common/store/inputs'; import { State } from '../../../../../common/store'; import { InputsRange } from '../../../../../common/store/inputs/model'; @@ -81,15 +77,10 @@ import { SecurityPageName } from '../../../../../app/types'; import { LinkButton } from '../../../../../common/components/links'; import { useFormatUrl } from '../../../../../common/components/link_to'; import { ExceptionsViewer } from '../../../../../common/components/exceptions/viewer'; -import { DEFAULT_INDEX_PATTERN, FILTERS_GLOBAL_HEIGHT } from '../../../../../../common/constants'; +import { DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants'; import { useFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; import { ExceptionListTypeEnum, ExceptionIdentifiers } from '../../../../../shared_imports'; -import { - getEventsViewerBodyHeight, - MIN_EVENTS_VIEWER_BODY_HEIGHT, -} from '../../../../../timelines/components/timeline/body/helpers'; -import { footerHeight } from '../../../../../timelines/components/timeline/footer'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { isThresholdRule } from '../../../../../../common/detection_engine/utils'; import { useRuleAsync } from '../../../../containers/detection_engine/rules/use_rule_async'; @@ -167,7 +158,6 @@ export const RuleDetailsPageComponent: FC = ({ const mlCapabilities = useMlCapabilities(); const history = useHistory(); const { formatUrl } = useFormatUrl(SecurityPageName.detections); - const { height: windowHeight } = useWindowSize(); const { globalFullScreen } = useFullScreen(); // TODO: Refactor license check + hasMlAdminPermissions to common check @@ -364,12 +354,9 @@ export const RuleDetailsPageComponent: FC = ({ {hasIndexWrite != null && !hasIndexWrite && } {userHasNoPermissions(canUserCRUD) && } {indicesExist ? ( - + <> - + @@ -507,17 +494,6 @@ export const RuleDetailsPageComponent: FC = ({ timelineId={TimelineId.detectionsRulesDetailsPage} canUserCRUD={canUserCRUD ?? false} defaultFilters={alertDefaultFilters} - eventsViewerBodyHeight={ - globalFullScreen - ? getEventsViewerBodyHeight({ - footerHeight, - headerHeight: EVENTS_VIEWER_HEADER_HEIGHT, - kibanaChromeHeight: globalHeaderHeightPx, - otherContentHeight: FILTERS_GLOBAL_HEIGHT, - windowHeight, - }) - : MIN_EVENTS_VIEWER_BODY_HEIGHT - } hasIndexWrite={hasIndexWrite ?? false} from={from} loading={loading} @@ -542,7 +518,7 @@ export const RuleDetailsPageComponent: FC = ({ )} {ruleDetailTab === RuleDetailTabs.failures && } - + ) : ( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index b6c1727ee6afac..34840b28266268 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -8,7 +8,6 @@ import { EuiHorizontalRule, EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useEffect, useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; import { SecurityPageName } from '../../../app/types'; import { UpdateDateRange } from '../../../common/components/charts/common'; @@ -102,12 +101,9 @@ const HostDetailsComponent = React.memo( return ( <> {indicesExist ? ( - + <> - + @@ -210,7 +206,7 @@ const HostDetailsComponent = React.memo( setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} /> - + ) : ( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index 1d0b73f80a69a1..e4e69443c510d2 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -8,7 +8,6 @@ import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; import { useParams } from 'react-router-dom'; import { SecurityPageName } from '../../app/types'; @@ -96,12 +95,9 @@ export const HostsComponent = React.memo( return ( <> {indicesExist ? ( - + <> - + @@ -156,7 +152,7 @@ export const HostsComponent = React.memo( hostsPagePath={hostsPagePath} /> - + ) : ( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index 962c227d6b6700..cea987db485f4b 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -6,7 +6,6 @@ import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux'; -import { useWindowSize } from 'react-use'; import { TimelineId } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; @@ -22,15 +21,7 @@ import { useFullScreen } from '../../../common/containers/use_full_screen'; import * as i18n from '../translations'; import { HistogramType } from '../../../graphql/types'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; -import { - getEventsViewerBodyHeight, - getInvestigateInResolverAction, - MIN_EVENTS_VIEWER_BODY_HEIGHT, -} from '../../../timelines/components/timeline/body/helpers'; -import { FILTERS_GLOBAL_HEIGHT } from '../../../../common/constants'; -import { globalHeaderHeightPx } from '../../../app/home'; -import { EVENTS_VIEWER_HEADER_HEIGHT } from '../../../common/components/events_viewer/events_viewer'; -import { footerHeight } from '../../../timelines/components/timeline/footer'; +import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; const EVENTS_HISTOGRAM_ID = 'eventsOverTimeQuery'; @@ -71,7 +62,6 @@ export const EventsQueryTabBody = ({ }: HostsComponentsQueryProps) => { const { initializeTimeline } = useManageTimeline(); const dispatch = useDispatch(); - const { height: windowHeight } = useWindowSize(); const { globalFullScreen } = useFullScreen(); useEffect(() => { initializeTimeline({ @@ -108,17 +98,6 @@ export const EventsQueryTabBody = ({ {indicesExist ? ( - - + <> + @@ -260,7 +259,7 @@ export const IPDetailsComponent: React.FC - + ) : ( diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index f516f2a2de346d..601bae89f7a4d8 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -9,7 +9,6 @@ import { noop } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { useParams } from 'react-router-dom'; -import { StickyContainer } from 'react-sticky'; import { esQuery } from '../../../../../../src/plugins/data/public'; import { SecurityPageName } from '../../app/types'; @@ -104,12 +103,9 @@ const NetworkComponent = React.memo( return ( <> {indicesExist ? ( - + <> - + @@ -180,7 +176,7 @@ const NetworkComponent = React.memo( )} - + ) : ( diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 1b743c259555ac..520fd6c4597055 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -7,7 +7,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useState, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; import { Query, Filter } from 'src/plugins/data/public'; import styled from 'styled-components'; @@ -70,8 +69,8 @@ const OverviewComponent: React.FC = ({ return ( <> {indicesExist ? ( - - + <> + @@ -140,7 +139,7 @@ const OverviewComponent: React.FC = ({ - + ) : ( )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap index 4bf0033bcb430d..46c9fbb5240663 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap @@ -2,7 +2,6 @@ exports[`Flyout rendering it renders correctly against snapshot 1`] = ` ({ StatefulTimeline: () =>
, })); -const testFlyoutHeight = 980; const usersViewing = ['elastic']; describe('Flyout', () => { @@ -39,7 +38,7 @@ describe('Flyout', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(wrapper.find('Flyout')).toMatchSnapshot(); @@ -48,7 +47,7 @@ describe('Flyout', () => { test('it renders the default flyout state as a button', () => { const wrapper = mount( - + ); @@ -69,7 +68,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -94,7 +93,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -117,7 +116,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -127,7 +126,7 @@ describe('Flyout', () => { test('it hides the data providers badge when the timeline does NOT have data providers', () => { const wrapper = mount( - + ); @@ -152,7 +151,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -167,7 +166,6 @@ describe('Flyout', () => { ` Visible.displayName = 'Visible'; interface OwnProps { - flyoutHeight: number; timelineId: string; usersViewing: string[]; } @@ -44,7 +43,7 @@ interface OwnProps { type Props = OwnProps & ProsFromRedux; export const FlyoutComponent = React.memo( - ({ dataProviders, flyoutHeight, show = true, showTimeline, timelineId, usersViewing, width }) => { + ({ dataProviders, show = true, showTimeline, timelineId, usersViewing, width }) => { const handleClose = useCallback(() => showTimeline({ id: timelineId, show: false }), [ showTimeline, timelineId, @@ -57,12 +56,7 @@ export const FlyoutComponent = React.memo( return ( <> - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap index d30fd6f31012c1..f24ef3448d03f9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap @@ -2,7 +2,6 @@ exports[`Pane renders correctly against snapshot 1`] = ` { test('renders correctly against snapshot', () => { const EmptyComponent = shallow( - + {'I am a child of flyout'} @@ -33,12 +27,7 @@ describe('Pane', () => { test('it should NOT let the flyout expand to take up the full width of the element that contains it', () => { const wrapper = mount( - + {'I am a child of flyout'} @@ -50,12 +39,7 @@ describe('Pane', () => { test('it should render a resize handle', () => { const wrapper = mount( - + {'I am a child of flyout'} @@ -67,12 +51,7 @@ describe('Pane', () => { test('it should render children', () => { const wrapper = mount( - + {'I am a mock body'} diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index 3f842bcc2eb68d..7528468ef65222 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -22,7 +22,6 @@ const minWidthPixels = 550; // do not allow the flyout to shrink below this widt const maxWidthPercent = 95; // do not allow the flyout to grow past this percentage of the view interface FlyoutPaneComponentProps { children: React.ReactNode; - flyoutHeight: number; onClose: () => void; timelineId: string; width: number; diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 97d1d11395c7f5..ededf701529683 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -34,8 +34,8 @@ import { useAllCasesModal } from '../../../cases/components/use_all_cases_modal' import * as i18n from './translations'; -const OverlayContainer = styled.div<{ bodyHeight?: number }>` - height: ${({ bodyHeight }) => (bodyHeight ? `${bodyHeight}px` : 'auto')}; +const OverlayContainer = styled.div` + height: 100%; width: 100%; display: flex; flex-direction: column; @@ -50,7 +50,6 @@ const FullScreenButtonIcon = styled(EuiButtonIcon)` `; interface OwnProps { - bodyHeight?: number; graphEventId?: string; timelineId: string; timelineType: TimelineType; @@ -97,7 +96,6 @@ const Navigation = ({ ); const GraphOverlayComponent = ({ - bodyHeight, graphEventId, status, timelineId, @@ -140,7 +138,7 @@ const GraphOverlayComponent = ({ ]); return ( - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts index 73b5a58ef7b657..b62888fbf84272 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts @@ -135,38 +135,3 @@ export const getInvestigateInResolverAction = ({ dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: eventId })), width: DEFAULT_ICON_BUTTON_WIDTH, }); - -/** - * The minimum height of a timeline-based events viewer body, as seen in several - * views, e.g. `Detections`, `Events`, `External events`, etc - */ -export const MIN_EVENTS_VIEWER_BODY_HEIGHT = 500; // px - -interface GetEventsViewerBodyHeightParams { - /** the height of the header, e.g. the section containing "`Showing n event / alerts`, and `Open` / `In progress` / `Closed` filters" */ - headerHeight: number; - /** the height of the footer, e.g. "`25 of 100 events / alerts`, `Load More`, `Updated n minutes ago`" */ - footerHeight: number; - /** the height of the global Kibana chrome, common throughout the app */ - kibanaChromeHeight: number; - /** the (combined) height of other non-events viewer content, e.g. the global search / filter bar in full screen mode */ - otherContentHeight: number; - /** the full height of the window */ - windowHeight: number; -} - -export const getEventsViewerBodyHeight = ({ - footerHeight, - headerHeight, - kibanaChromeHeight, - otherContentHeight, - windowHeight, -}: GetEventsViewerBodyHeightParams) => { - if (windowHeight === 0 || !isFinite(windowHeight)) { - return MIN_EVENTS_VIEWER_BODY_HEIGHT; - } - - const combinedHeights = kibanaChromeHeight + otherContentHeight + headerHeight + footerHeight; - - return Math.max(MIN_EVENTS_VIEWER_BODY_HEIGHT, windowHeight - combinedHeights); -}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 5a98263cbd3fd3..456c1ee5414711 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -22,7 +22,6 @@ import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { SELECTOR_TIMELINE_BODY_CLASS_NAME, TimelineBody } from '../styles'; import { TimelineType } from '../../../../../common/types/timeline'; -const testBodyHeight = 700; const mockGetNotesByIds = (eventId: string[]) => []; const mockSort: Sort = { columnId: '@timestamp', @@ -65,7 +64,6 @@ describe('Body', () => { data: mockTimelineData, docValueFields: [], eventIdToNoteIds: {}, - height: testBodyHeight, id: 'timeline-test', isSelectAllChecked: false, getNotesByIds: mockGetNotesByIds, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index e971dc6c8e1e2c..6f578ffe3e9568 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -42,7 +42,6 @@ export interface BodyProps { docValueFields: DocValueFields[]; getNotesByIds: (noteIds: string[]) => Note[]; graphEventId?: string; - height?: number; id: string; isEventViewer?: boolean; isSelectAllChecked: boolean; @@ -85,7 +84,6 @@ export const Body = React.memo( eventIdToNoteIds, getNotesByIds, graphEventId, - height, id, isEventViewer = false, isSelectAllChecked, @@ -129,17 +127,11 @@ export const Body = React.memo( return ( <> {graphEventId && ( - + )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index 550a4adf713c60..15fa13b1a08f12 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -42,7 +42,6 @@ interface OwnProps { browserFields: BrowserFields; data: TimelineItem[]; docValueFields: DocValueFields[]; - height?: number; id: string; isEventViewer?: boolean; sort: Sort; @@ -63,7 +62,6 @@ const StatefulBodyComponent = React.memo( docValueFields, eventIdToNoteIds, excludedRowRendererIds, - height, id, isEventViewer = false, isSelectAllChecked, @@ -199,7 +197,6 @@ const StatefulBodyComponent = React.memo( eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} graphEventId={graphEventId} - height={height} id={id} isEventViewer={isEventViewer} isSelectAllChecked={isSelectAllChecked} @@ -234,7 +231,6 @@ const StatefulBodyComponent = React.memo( prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && prevProps.graphEventId === nextProps.graphEventId && deepEqual(prevProps.notesById, nextProps.notesById) && - prevProps.height === nextProps.height && prevProps.id === nextProps.id && prevProps.isEventViewer === nextProps.isEventViewer && prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx index 4e1595eef984c0..3169201b12c7bd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx @@ -68,7 +68,7 @@ const FooterContainer = styled(EuiFlexGroup).attrs(({ height }) => ( height: `${height}px`, }, }))` - flex: 0; + flex: 0 0 auto; `; FooterContainer.displayName = 'FooterContainer'; From 3cce216f2e5c3c1167723414309ae78506f9c614 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Mon, 3 Aug 2020 19:24:48 -0700 Subject: [PATCH 38/74] [Ingest Manager] Improve agent config list -> detail UI transition (#73566) * Improve agent config detail page loading state * Show ID instead of loading icon * Fix types --- .../agent_config/details_page/index.tsx | 261 +++++++++--------- 1 file changed, 133 insertions(+), 128 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx index 0e65cb80f07c49..b87bd66cce0eaf 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx @@ -10,7 +10,6 @@ import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, - EuiCallOut, EuiText, EuiSpacer, EuiButtonEmpty, @@ -24,7 +23,7 @@ import styled from 'styled-components'; import { AgentConfig, AgentConfigDetailsDeployAgentAction } from '../../../types'; import { PAGE_ROUTING_PATHS } from '../../../constants'; import { useGetOneAgentConfig, useLink, useBreadcrumbs, useCore } from '../../../hooks'; -import { Loading } from '../../../components'; +import { Loading, Error } from '../../../components'; import { WithHeaderLayout } from '../../../layouts'; import { ConfigRefreshContext, useGetAgentStatus, AgentStatusRefreshContext } from './hooks'; import { LinkedAgentCount, AgentConfigActionMenu } from '../components'; @@ -109,97 +108,98 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => { }, [routeState, navigateToApp]); const headerRightContent = useMemo( - () => ( - - {[ - { - label: i18n.translate('xpack.ingestManager.configDetails.summary.revision', { - defaultMessage: 'Revision', - }), - content: agentConfig?.revision ?? 0, - }, - { isDivider: true }, - { - label: i18n.translate('xpack.ingestManager.configDetails.summary.package_configs', { - defaultMessage: 'Integrations', - }), - content: ( - - ), - }, - { isDivider: true }, - { - label: i18n.translate('xpack.ingestManager.configDetails.summary.usedBy', { - defaultMessage: 'Used by', - }), - content: ( - - ), - }, - { isDivider: true }, - { - label: i18n.translate('xpack.ingestManager.configDetails.summary.lastUpdated', { - defaultMessage: 'Last updated on', - }), - content: - (agentConfig && ( - + agentConfig ? ( + + {[ + { + label: i18n.translate('xpack.ingestManager.configDetails.summary.revision', { + defaultMessage: 'Revision', + }), + content: agentConfig?.revision ?? 0, + }, + { isDivider: true }, + { + label: i18n.translate('xpack.ingestManager.configDetails.summary.package_configs', { + defaultMessage: 'Integrations', + }), + content: ( + - )) || - '', - }, - { isDivider: true }, - { - content: agentConfig && ( - { - history.push(getPath('configuration_details', { configId: newAgentConfig.id })); - }} - enrollmentFlyoutOpenByDefault={openEnrollmentFlyoutOpenByDefault} - onCancelEnrollment={ - routeState && routeState.onDoneNavigateTo - ? enrollmentCancelClickHandler - : undefined - } - /> - ), - }, - ].map((item, index) => ( - - {item.isDivider ?? false ? ( - - ) : item.label ? ( - - - {item.label} - - - {item.content} - - - ) : ( - item.content - )} - - ))} - - ), + ), + }, + { isDivider: true }, + { + label: i18n.translate('xpack.ingestManager.configDetails.summary.usedBy', { + defaultMessage: 'Used by', + }), + content: ( + + ), + }, + { isDivider: true }, + { + label: i18n.translate('xpack.ingestManager.configDetails.summary.lastUpdated', { + defaultMessage: 'Last updated on', + }), + content: + (agentConfig && ( + + )) || + '', + }, + { isDivider: true }, + { + content: agentConfig && ( + { + history.push(getPath('configuration_details', { configId: newAgentConfig.id })); + }} + enrollmentFlyoutOpenByDefault={openEnrollmentFlyoutOpenByDefault} + onCancelEnrollment={ + routeState && routeState.onDoneNavigateTo + ? enrollmentCancelClickHandler + : undefined + } + /> + ), + }, + ].map((item, index) => ( + + {item.isDivider ?? false ? ( + + ) : item.label ? ( + + + {item.label} + + + {item.content} + + + ) : ( + item.content + )} + + ))} + + ) : undefined, /* eslint-disable-next-line react-hooks/exhaustive-deps */ [agentConfig, configId, agentStatus] ); @@ -225,45 +225,50 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => { ]; }, [getHref, configId, tabId]); - if (redirectToAgentConfigList) { - return ; - } + const content = useMemo(() => { + if (redirectToAgentConfigList) { + return ; + } - if (isLoading) { - return ; - } + if (isLoading) { + return ; + } - if (error) { - return ( - - -

- {error.message} -

-
-
- ); - } + if (error) { + return ( + + } + error={error} + /> + ); + } - if (!agentConfig) { - return ( - - + } + error={i18n.translate('xpack.ingestManager.configDetails.configNotFoundErrorTitle', { + defaultMessage: "Config '{id}' not found", + values: { + id: configId, + }, + })} /> - - ); - } + ); + } + + return ; + }, [agentConfig, configId, error, isLoading, redirectToAgentConfigList]); return ( @@ -273,7 +278,7 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => { rightColumn={headerRightContent} tabs={(headerTabs as unknown) as EuiTabProps[]} > - + {content} From fb3370d99b2b8be5ba49b36b301fd9efd4502447 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 4 Aug 2020 09:17:03 +0200 Subject: [PATCH 39/74] updates file picker label (#74136) --- .../components/value_lists_management_modal/translations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts index 7063dca2341ca6..9beebdfb923dc1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts @@ -13,7 +13,7 @@ export const MODAL_TITLE = i18n.translate('xpack.securitySolution.lists.uploadVa export const FILE_PICKER_LABEL = i18n.translate( 'xpack.securitySolution.lists.uploadValueListDescription', { - defaultMessage: 'Upload single value lists to use while writing rules or rule exceptions.', + defaultMessage: 'Upload single value lists to use while writing rule exceptions.', } ); From e57f4d2b25ac95183e876ea0c305138ab119ea26 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 4 Aug 2020 10:50:32 +0300 Subject: [PATCH 40/74] [KP] update ES client migration guide (#73241) * add note about error format and FTR service * add notice about lack of response typings * more detailes example of search response type usage --- src/core/MIGRATION_EXAMPLES.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index d630fec652a375..3f34742e44861c 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -1082,7 +1082,7 @@ const { body } = await client.asInternalUser.get({ id: 'id' }); const { body } = await client.asInternalUser.get({ id: 'id' }); ``` -- the returned error types changed +- the returned error types changed There are no longer specific errors for every HTTP status code (such as `BadRequest` or `NotFound`). A generic `ResponseError` with the specific `statusCode` is thrown instead. @@ -1097,6 +1097,7 @@ try { if(e instanceof errors.NotFound) { // do something } + if(e.status === 401) {} } ``` @@ -1115,6 +1116,7 @@ try { if(e.name === 'ResponseError' && e.statusCode === 404) { // do something } + if(e.statusCode === 401) {...} } ``` @@ -1178,6 +1180,30 @@ const request = client.asCurrentUser.ping({}, { }); ``` +- the new client doesn't provide exhaustive typings for the response object yet. You might have to copy +response type definitions from the Legacy Elasticsearch library until https://github.com/elastic/elasticsearch-js/pull/970 merged. + +```ts +// platform provides a few typings for internal purposes +import { SearchResponse } from 'src/core/server'; +type SearchSource = {...}; +type SearchBody = SearchResponse; +const { body } = await client.search(...); +interface Info {...} +const { body } = await client.info(...); +``` + +- Functional tests are subject to migration to the new client as well. +before: +```ts +const client = getService('legacyEs'); +``` + +after: +```ts +const client = getService('es'); +``` + Please refer to the [Breaking changes list](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/breaking-changes.html) for more information about the changes between the legacy and new client. From fc04ef915269a754f35e2eba64a2088e29f1ab30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 4 Aug 2020 11:46:49 +0100 Subject: [PATCH 41/74] Observabilty overview APM section doesn't fully collapse (#74224) --- .../public/pages/overview/index.tsx | 78 ++++++++++--------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index 32bdb00577bd27..8870bcbc9fa38a 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -113,44 +113,46 @@ export function OverviewPage({ routeParams }: Props) { {/* Data sections */} {showDataSections && ( - - {hasData.infra_logs && ( - - - - )} - {hasData.infra_metrics && ( - - - - )} - {hasData.apm && ( - - - - )} - {hasData.uptime && ( - - - - )} - + + + {hasData.infra_logs && ( + + + + )} + {hasData.infra_metrics && ( + + + + )} + {hasData.apm && ( + + + + )} + {hasData.uptime && ( + + + + )} + + )} {/* Empty sections */} From 8e7d0b86cccce9052434f94e6cc369dab817964d Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 4 Aug 2020 07:36:21 -0400 Subject: [PATCH 42/74] [SECURITY] Bug do not allow saving when opening a timeline (#74139) * only saved timeline when you are duplicating them * fix unit test --- .../public/timelines/components/open_timeline/helpers.test.ts | 1 + .../public/timelines/components/open_timeline/helpers.ts | 2 +- .../public/timelines/store/timeline/actions.ts | 1 + .../security_solution/public/timelines/store/timeline/epic.ts | 3 ++- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index f4bd17005fed78..ac6c61b33b35ec 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -967,6 +967,7 @@ describe('helpers', () => { expect(dispatchAddTimeline).toHaveBeenCalledWith({ id: 'timeline-1', + savedTimeline: true, timeline: mockTimelineModel, }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index af289f94c9a0d5..27920fa297a484 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -374,7 +374,7 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli ruleNote, }: UpdateTimeline): (() => void) => () => { dispatch(dispatchSetTimelineRangeDatePicker({ from, to })); - dispatch(dispatchAddTimeline({ id, timeline })); + dispatch(dispatchAddTimeline({ id, timeline, savedTimeline: duplicate })); if ( timeline.kqlQuery != null && timeline.kqlQuery.filterQuery != null && diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index faeef432ea4229..ebee0f5cae9a66 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -104,6 +104,7 @@ export const updateTimeline = actionCreator<{ export const addTimeline = actionCreator<{ id: string; timeline: TimelineModel; + savedTimeline?: boolean; }>('ADD_TIMELINE'); export const setInsertTimeline = actionCreator('SET_INSERT_TIMELINE'); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts index 7757794c6dc9a7..ad849c3a995b33 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts @@ -6,6 +6,7 @@ import { get, + getOr, has, merge as mergeObject, set, @@ -172,7 +173,7 @@ export const createTimelineEpic = (): Epic< myEpicTimelineId.setTimelineVersion(addNewTimeline.version); myEpicTimelineId.setTemplateTimelineId(addNewTimeline.templateTimelineId); myEpicTimelineId.setTemplateTimelineVersion(addNewTimeline.templateTimelineVersion); - return true; + return getOr(false, 'payload.savedTimeline', action); } else if ( timelineActionsType.includes(action.type) && !timelineObj.isLoading && From 5c741a1e0056d46612c6e6633adef3dc1173472c Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 4 Aug 2020 07:37:22 -0400 Subject: [PATCH 43/74] reset datepicker of timeline (#74210) --- .../properties/use_create_timeline.test.tsx | 46 ++++++++++++++++++- .../properties/use_create_timeline.tsx | 25 ++++++++-- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx index 8f548f16cf1d6b..5b3bc72fc37ca3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx @@ -4,18 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import { act as actDom } from 'react-dom/test-utils'; + import { renderHook, act } from '@testing-library/react-hooks'; -import { shallow } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import { TimelineType } from '../../../../../common/types/timeline'; import { TestProviders } from '../../../../common/mock'; import { useCreateTimelineButton } from './use_create_timeline'; +const mockDispatch = jest.fn(); + jest.mock('react-redux', () => { const actual = jest.requireActual('react-redux'); return { ...actual, - useDispatch: jest.fn(), + useDispatch: () => mockDispatch, + useSelector: () => ({ + kind: 'relative', + fromStr: 'now-24h', + toStr: 'now', + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', + }), }; }); @@ -65,4 +76,35 @@ describe('useCreateTimelineButton', () => { expect(wrapper.find('[data-test-subj="timeline-new"]').exists()).toBeTruthy(); }); }); + + test('Make sure that timeline reset to the global date picker', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => useCreateTimelineButton({ timelineId: mockId, timelineType }), + { wrapper: wrapperContainer } + ); + + await waitForNextUpdate(); + const button = result.current.getButton({ outline: false, title: 'mock title' }); + actDom(() => { + const wrapper = mount(button); + wrapper.update(); + + wrapper.find('[data-test-subj="timeline-new"]').first().simulate('click'); + + expect(mockDispatch.mock.calls[0][0].type).toEqual( + 'x-pack/security_solution/local/timeline/CREATE_TIMELINE' + ); + expect(mockDispatch.mock.calls[1][0].type).toEqual( + 'x-pack/security_solution/local/inputs/ADD_GLOBAL_LINK_TO' + ); + expect(mockDispatch.mock.calls[2][0].type).toEqual( + 'x-pack/security_solution/local/inputs/ADD_TIMELINE_LINK_TO' + ); + expect(mockDispatch.mock.calls[3][0].type).toEqual( + 'x-pack/security_solution/local/inputs/SET_RELATIVE_RANGE_DATE_PICKER' + ); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx index f418491ac4e477..97f3b1df011ffd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { timelineActions } from '../../../store/timeline'; @@ -14,6 +14,7 @@ import { TimelineType, TimelineTypeLiteral, } from '../../../../../common/types/timeline'; +import { inputsActions, inputsSelectors } from '../../../../common/store/inputs'; export const useCreateTimelineButton = ({ timelineId, @@ -26,13 +27,12 @@ export const useCreateTimelineButton = ({ }) => { const dispatch = useDispatch(); const { timelineFullScreen, setTimelineFullScreen } = useFullScreen(); - + const globalTimeRange = useSelector(inputsSelectors.globalTimeRangeSelector); const createTimeline = useCallback( ({ id, show }) => { if (id === TimelineId.active && timelineFullScreen) { setTimelineFullScreen(false); } - dispatch( timelineActions.createTimeline({ id, @@ -41,8 +41,25 @@ export const useCreateTimelineButton = ({ timelineType, }) ); + dispatch(inputsActions.addGlobalLinkTo({ linkToId: 'timeline' })); + dispatch(inputsActions.addTimelineLinkTo({ linkToId: 'global' })); + if (globalTimeRange.kind === 'absolute') { + dispatch( + inputsActions.setAbsoluteRangeDatePicker({ + ...globalTimeRange, + id: 'timeline', + }) + ); + } else if (globalTimeRange.kind === 'relative') { + dispatch( + inputsActions.setRelativeRangeDatePicker({ + ...globalTimeRange, + id: 'timeline', + }) + ); + } }, - [dispatch, setTimelineFullScreen, timelineFullScreen, timelineType] + [dispatch, globalTimeRange, setTimelineFullScreen, timelineFullScreen, timelineType] ); const handleButtonClick = useCallback(() => { From 53131313df49ee7eeac08e856e81eb6e617ac7e5 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Tue, 4 Aug 2020 08:19:02 -0400 Subject: [PATCH 44/74] [kbn-storybook] Kill tsdocgen for immediate perf improvement (#74059) * [kbn-storybook] Kill tsdocgen for immediate perf improvement * Addressing feedback --- .../storybook_config/webpack.config.js | 241 +++++++++--------- 1 file changed, 117 insertions(+), 124 deletions(-) diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js index 0a9977463aee8c..7b43d106417b25 100644 --- a/packages/kbn-storybook/storybook_config/webpack.config.js +++ b/packages/kbn-storybook/storybook_config/webpack.config.js @@ -19,6 +19,7 @@ const { parse, resolve } = require('path'); const webpack = require('webpack'); +const webpackMerge = require('webpack-merge'); const { stringifyRequest } = require('loader-utils'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const { REPO_ROOT, DLL_DIST_DIR } = require('../lib/constants'); @@ -26,145 +27,137 @@ const { REPO_ROOT, DLL_DIST_DIR } = require('../lib/constants'); const { currentConfig } = require('../../../built_assets/storybook/current.config'); // Extend the Storybook Webpack config with some customizations -module.exports = async ({ config }) => { - // Find and alter the CSS rule to replace the Kibana public path string with a path - // to the route we've added in middleware.js - const cssRule = config.module.rules.find((rule) => rule.test.source.includes('.css$')); - cssRule.use.push({ - loader: 'string-replace-loader', - options: { - search: '__REPLACE_WITH_PUBLIC_PATH__', - replace: '/', - flags: 'g', - }, - }); - - // Include the React preset from Kibana for Storybook JS files. - config.module.rules.push({ - test: /\.js$/, - exclude: /node_modules/, - loaders: 'babel-loader', - options: { - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], - }, - }); - - config.module.rules.push({ - test: /\.(html|md|txt|tmpl)$/, - use: { - loader: 'raw-loader', - }, - }); - - // Handle Typescript files - config.module.rules.push({ - test: /\.tsx?$/, - use: [ - { - loader: 'babel-loader', - options: { - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], +module.exports = async ({ config: storybookConfig }) => { + let config = { + module: { + rules: [ + // Include the React preset from Kibana for JS(X) and TS(X) + { + test: /\.(j|t)sx?$/, + exclude: /node_modules/, + loaders: 'babel-loader', + options: { + presets: [require.resolve('@kbn/babel-preset/webpack_preset')], + }, }, - }, - ], - }); - - // Parse props data for .tsx files - config.module.rules.push({ - test: /\.tsx$/, - // Exclude example files, as we don't display props info for them - exclude: /\.examples.tsx$/, - use: [ - // Parse TS comments to create Props tables in the UI - require.resolve('react-docgen-typescript-loader'), - ], - }); - - // Enable SASS - config.module.rules.push({ - test: /\.scss$/, - exclude: /\.module.(s(a|c)ss)$/, - use: [ - { loader: 'style-loader' }, - { loader: 'css-loader', options: { importLoaders: 2 } }, - { - loader: 'postcss-loader', - options: { - config: { - path: require.resolve('@kbn/optimizer/postcss.config.js'), + { + test: /\.(html|md|txt|tmpl)$/, + use: { + loader: 'raw-loader', }, }, - }, - { - loader: 'resolve-url-loader', - options: { - // If you don't have arguments (_, __) to the join function, the - // resolve-url-loader fails with a loader misconfiguration error. - // - // eslint-disable-next-line no-unused-vars - join: (_, __) => (uri, base) => { - if (!base || !parse(base).dir.includes('legacy')) { - return null; - } + // Parse props data for .tsx files + // This is notoriously slow, and is making Storybook unusable. Disabling for now. + // See: https://github.com/storybookjs/storybook/issues/7998 + // + // { + // test: /\.tsx$/, + // // Exclude example files, as we don't display props info for them + // exclude: /\.stories.tsx$/, + // use: [ + // // Parse TS comments to create Props tables in the UI + // require.resolve('react-docgen-typescript-loader'), + // ], + // }, + { + test: /\.scss$/, + exclude: /\.module.(s(a|c)ss)$/, + use: [ + { loader: 'style-loader' }, + { loader: 'css-loader', options: { importLoaders: 2 } }, + { + loader: 'postcss-loader', + options: { + config: { + path: require.resolve('@kbn/optimizer/postcss.config.js'), + }, + }, + }, + { + loader: 'resolve-url-loader', + options: { + // If you don't have arguments (_, __) to the join function, the + // resolve-url-loader fails with a loader misconfiguration error. + // + // eslint-disable-next-line no-unused-vars + join: (_, __) => (uri, base) => { + if (!base || !parse(base).dir.includes('legacy')) { + return null; + } - // URIs on mixins in src/legacy/public/styles need to be resolved. - if (uri.startsWith('ui/assets')) { - return resolve(REPO_ROOT, 'src/core/server/core_app/', uri.replace('ui/', '')); - } + // URIs on mixins in src/legacy/public/styles need to be resolved. + if (uri.startsWith('ui/assets')) { + return resolve(REPO_ROOT, 'src/core/server/core_app/', uri.replace('ui/', '')); + } - return null; - }, + return null; + }, + }, + }, + { + loader: 'sass-loader', + options: { + prependData(loaderContext) { + return `@import ${stringifyRequest( + loaderContext, + resolve(REPO_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') + )};\n`; + }, + sassOptions: { + includePaths: [resolve(REPO_ROOT, 'node_modules')], + }, + }, + }, + ], }, - }, - { - loader: 'sass-loader', - options: { - prependData(loaderContext) { - return `@import ${stringifyRequest( - loaderContext, - resolve(REPO_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') - )};\n`; + ], + }, + plugins: [ + // Reference the built DLL file of static(ish) dependencies, which are removed + // during kbn:bootstrap and rebuilt if missing. + new webpack.DllReferencePlugin({ + manifest: resolve(DLL_DIST_DIR, 'manifest.json'), + context: REPO_ROOT, + }), + // Copy the DLL files to the Webpack build for use in the Storybook UI + + new CopyWebpackPlugin({ + patterns: [ + { + from: resolve(DLL_DIST_DIR, 'dll.js'), + to: 'dll.js', }, - sassOptions: { - includePaths: [resolve(REPO_ROOT, 'node_modules')], + { + from: resolve(DLL_DIST_DIR, 'dll.css'), + to: 'dll.css', }, - }, - }, + ], + }), ], - }); - - // Reference the built DLL file of static(ish) dependencies, which are removed - // during kbn:bootstrap and rebuilt if missing. - config.plugins.push( - new webpack.DllReferencePlugin({ - manifest: resolve(DLL_DIST_DIR, 'manifest.json'), - context: REPO_ROOT, - }) - ); + resolve: { + // Tell Webpack about the ts/x extensions + extensions: ['.ts', '.tsx', '.scss'], + }, + }; - // Copy the DLL files to the Webpack build for use in the Storybook UI - config.plugins.push( - new CopyWebpackPlugin({ - patterns: [ - { - from: resolve(DLL_DIST_DIR, 'dll.js'), - to: 'dll.js', - }, - { - from: resolve(DLL_DIST_DIR, 'dll.css'), - to: 'dll.css', - }, - ], - }) - ); + // Find and alter the CSS rule to replace the Kibana public path string with a path + // to the route we've added in middleware.js + const cssRule = storybookConfig.module.rules.find((rule) => rule.test.source.includes('.css$')); + cssRule.use.push({ + loader: 'string-replace-loader', + options: { + search: '__REPLACE_WITH_PUBLIC_PATH__', + replace: '/', + flags: 'g', + }, + }); - // Tell Webpack about the ts/x extensions - config.resolve.extensions.push('.ts', '.tsx', '.scss'); + config = webpackMerge(storybookConfig, config); // Load custom Webpack config specified by a plugin. if (currentConfig.webpackHook) { // eslint-disable-next-line import/no-dynamic-require - config = await require(currentConfig.webpackHook)({ config }); + return await require(currentConfig.webpackHook)({ config }); } return config; From c78da24708e55d22a1ea54e6de99f608a7e35b27 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 4 Aug 2020 14:44:16 +0200 Subject: [PATCH 45/74] [ Functional Tests ] TVSB charts flaky tests on index pattern switch fix (#74110) Co-authored-by: Elastic Machine --- test/functional/apps/visualize/_tsvb_chart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 7e1f88650cbba0..f1082bf618b90d 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -130,7 +130,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const fromTime = 'Oct 22, 2018 @ 00:00:00.000'; const toTime = 'Oct 28, 2018 @ 23:59:59.999'; // Sometimes popovers take some time to appear in Firefox (#71979) - await retry.try(async () => { + await retry.tryForTime(20000, async () => { await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.visualBuilder.setIndexPatternValue('kibana_sample_data_flights'); await PageObjects.visualBuilder.selectIndexPatternTimeField('timestamp'); From 73077c3d19cbece3449780ad97676f03df18867a Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 4 Aug 2020 06:54:47 -0600 Subject: [PATCH 46/74] Fixes one more spot where we forgot to add plumbing for the strict_date_optional_time (#74211) ## Summary Related closed issues: https://github.com/elastic/kibana/issues/58965 https://github.com/elastic/kibana/pull/70713 If you add a custom mapping and go to the hosts details page you will get an error toaster: Screen Shot 2020-08-03 at 7 53 16 PM If running local host you can configure your index patterns to use a custom one I setup with custom date time formats and a single record which can cause this: Screen Shot 2020-08-03 at 7 50 12 PM Then visit this URL and set your date time to go backwards by 1 year ```ts http://localhost:5601/app/security/hosts/app/security/hosts/MacBook-Pro.local/alerts ``` And with the fix you no longer get the error toaster. --- .../security_solution/server/lib/hosts/query.detail_host.dsl.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts b/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts index ee0d98c45c4434..10dcb7ee7e7432 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts @@ -26,6 +26,7 @@ export const buildHostOverviewQuery = ({ { range: { [timestamp]: { + format: 'strict_date_optional_time', gte: from, lte: to, }, From 4f2209b209e3bbe460f2226a91b3dd437676e57d Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Tue, 4 Aug 2020 06:20:57 -0700 Subject: [PATCH 47/74] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20clean=20up=20a?= =?UTF-8?q?nd=20simplify=20replace=20panel=20input=20update=20(#74076)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../actions/replace_panel_flyout.tsx | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx b/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx index e4a98ffac7a554..0000f63c48c2db 100644 --- a/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx @@ -69,31 +69,33 @@ export class ReplacePanelFlyout extends React.Component { }; public onReplacePanel = async (savedObjectId: string, type: string, name: string) => { - const originalPanels = this.props.container.getInput().panels; - const filteredPanels = { ...originalPanels }; + const { panelToRemove, container } = this.props; + const { w, h, x, y } = (container.getInput().panels[ + panelToRemove.id + ] as DashboardPanelState).gridData; - const nnw = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.w; - const nnh = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.h; - const nnx = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.x; - const nny = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.y; - - // add the new view - const newObj = await this.props.container.addNewEmbeddable(type, { + const { id } = await container.addNewEmbeddable(type, { savedObjectId, }); - const finalPanels = _.cloneDeep(this.props.container.getInput().panels); - (finalPanels[newObj.id] as DashboardPanelState).gridData.w = nnw; - (finalPanels[newObj.id] as DashboardPanelState).gridData.h = nnh; - (finalPanels[newObj.id] as DashboardPanelState).gridData.x = nnx; - (finalPanels[newObj.id] as DashboardPanelState).gridData.y = nny; - - // delete the old view - delete finalPanels[this.props.panelToRemove.id]; - - // apply changes - this.props.container.updateInput({ panels: finalPanels }); - this.props.container.reload(); + const { [panelToRemove.id]: omit, ...panels } = container.getInput().panels; + + container.updateInput({ + panels: { + ...panels, + [id]: { + ...panels[id], + gridData: { + ...(panels[id] as DashboardPanelState).gridData, + w, + h, + x, + y, + }, + } as DashboardPanelState, + }, + }); + container.reload(); this.showToast(name); this.props.onClose(); From 23fedcf636aac1fd9af0858c9adf534f29db5418 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Tue, 4 Aug 2020 06:25:53 -0700 Subject: [PATCH 48/74] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20navigate=20from=20?= =?UTF-8?q?maps=20embeddable=20to=20app=20in=20SPA=20way=20(#74102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/maps/public/embeddable/map_embeddable.tsx | 2 ++ .../maps/public/embeddable/map_embeddable_factory.ts | 10 +++++++++- x-pack/plugins/maps/public/embeddable/types.ts | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 137b3a31b795bf..616d06a5c7b19a 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -81,6 +81,8 @@ export class MapEmbeddable extends Embeddable Date: Tue, 4 Aug 2020 15:56:18 +0200 Subject: [PATCH 49/74] [Security Solution] Fix Fullscreen view (#74236) --- x-pack/plugins/security_solution/public/app/home/index.tsx | 1 - .../security_solution/public/common/components/page/index.tsx | 1 + .../timelines/components/open_timeline/use_timeline_types.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index f230566fa08e6a..7c287646ba7ac5 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -27,7 +27,6 @@ const SecuritySolutionAppWrapper = styled.div` SecuritySolutionAppWrapper.displayName = 'SecuritySolutionAppWrapper'; const Main = styled.main` - position: relative; overflow: auto; flex: 1; `; diff --git a/x-pack/plugins/security_solution/public/common/components/page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page/index.tsx index 6c49ce7453ce90..140429dc4abd78 100644 --- a/x-pack/plugins/security_solution/public/common/components/page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page/index.tsx @@ -73,6 +73,7 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar #kibana-body { height: 100%; + overflow-y: hidden; > .content { height: 100%; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx index 1ffa626b013114..ec02124ae43d32 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx @@ -100,7 +100,7 @@ export const useTimelineTypes = ({ (tabId, tabStyle: TimelineTabsStyle) => { setTimelineTypes((prevTimelineTypes) => { if (tabId === prevTimelineTypes && tabStyle === TimelineTabsStyle.filter) { - return null; + return tabId === TimelineType.default ? TimelineType.template : TimelineType.default; } else if (prevTimelineTypes !== tabId) { setTimelineTypes(tabId); } From 7208a5746abbab6d8377294a7b95701faa1aaf3f Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Tue, 4 Aug 2020 09:18:47 -0500 Subject: [PATCH 50/74] [Canvas] Fix Custom Element functional test (and remove skip) (#73727) * Remove skip of custom elements --- x-pack/test/functional/apps/canvas/custom_elements.ts | 4 ++-- x-pack/test/functional/page_objects/canvas_page.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional/apps/canvas/custom_elements.ts b/x-pack/test/functional/apps/canvas/custom_elements.ts index 20ad045d0a653a..33db56751285ef 100644 --- a/x-pack/test/functional/apps/canvas/custom_elements.ts +++ b/x-pack/test/functional/apps/canvas/custom_elements.ts @@ -19,8 +19,7 @@ export default function canvasCustomElementTest({ const PageObjects = getPageObjects(['canvas', 'common']); const find = getService('find'); - // FLAKY: https://github.com/elastic/kibana/issues/63339 - describe.skip('custom elements', function () { + describe('custom elements', function () { this.tags('skipFirefox'); before(async () => { @@ -66,6 +65,7 @@ export default function canvasCustomElementTest({ // ensure the custom element is the one expected and click it to add to the workpad const customElement = await find.byCssSelector('.canvasElementCard__wrapper'); const elementName = await customElement.findByCssSelector('.euiCard__title'); + expect(await elementName.getVisibleText()).to.contain('My New Element'); customElement.click(); diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts index f08d1e6b7fef47..23b5057573b3b7 100644 --- a/x-pack/test/functional/page_objects/canvas_page.ts +++ b/x-pack/test/functional/page_objects/canvas_page.ts @@ -8,10 +8,11 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; -export function CanvasPageProvider({ getService }: FtrProviderContext) { +export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); const browser = getService('browser'); + const PageObjects = getPageObjects(['common']); return { async enterFullscreen() { @@ -58,6 +59,8 @@ export function CanvasPageProvider({ getService }: FtrProviderContext) { async openSavedElementsModal() { await testSubjects.click('add-element-button'); await testSubjects.click('saved-elements-menu-option'); + + await PageObjects.common.sleep(1000); // give time for modal animation to complete }, async closeSavedElementsModal() { await testSubjects.click('saved-elements-modal-close-button'); From f3fbb7d79f08a518292f32e67ffa9d99ef22606b Mon Sep 17 00:00:00 2001 From: Ben Skelker <54019610+benskelker@users.noreply.github.com> Date: Tue, 4 Aug 2020 17:42:51 +0300 Subject: [PATCH 51/74] [Security Solution]Updates exception options and setting text (#73769) * updates exception text * updates modal text Co-authored-by: Elastic Machine --- .../components/exceptions/add_exception_modal/translations.ts | 2 +- .../exceptions/edit_exception_modal/translations.ts | 2 +- .../public/common/components/exceptions/translations.ts | 4 ++-- .../public/detections/components/alerts_table/translations.ts | 2 +- .../detections/components/rules/step_about_rule/schema.tsx | 2 +- .../components/rules/step_about_rule/translations.ts | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/translations.ts index abc296e9c0e1a0..39162844167073 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/translations.ts @@ -56,7 +56,7 @@ export const ENDPOINT_QUARANTINE_TEXT = i18n.translate( 'xpack.securitySolution.exceptions.addException.endpointQuarantineText', { defaultMessage: - 'Any file in quarantine on any endpoint that matches the attribute(s) selected will automatically be restored to its original location. This exception will apply to any rule that is linked to the Global Endpoint Exception List.', + 'On all Endpoint hosts, quarantined files that match the exception are automatically restored to their original locations. This exception applies to all rules using Endpoint exceptions.', } ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts index c5b6fc8a6a9ae3..09e0a75d215730 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts @@ -65,7 +65,7 @@ export const ENDPOINT_QUARANTINE_TEXT = i18n.translate( 'xpack.securitySolution.exceptions.editException.endpointQuarantineText', { defaultMessage: - 'Any file in quarantine on any endpoint that matches the attribute(s) selected will automatically be restored to its original location. This exception will apply to any rule that is linked to the Global Endpoint Exception List.', + 'On all Endpoint hosts, quarantined files that match the exception are automatically restored to their original locations. This exception applies to all rules using Endpoint exceptions.', } ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts index e68b903266428f..13e9d0df549f8a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts @@ -77,14 +77,14 @@ export const ADD_EXCEPTION_LABEL = i18n.translate( export const ADD_TO_ENDPOINT_LIST = i18n.translate( 'xpack.securitySolution.exceptions.viewer.addToEndpointListLabel', { - defaultMessage: 'Add to endpoint list', + defaultMessage: 'Add Endpoint exception', } ); export const ADD_TO_DETECTIONS_LIST = i18n.translate( 'xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel', { - defaultMessage: 'Add to detections list', + defaultMessage: 'Add rule exception', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index e5e8635b9e7999..3d6c3dc0a7a8e5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -118,7 +118,7 @@ export const ACTION_INVESTIGATE_IN_TIMELINE = i18n.translate( export const ACTION_ADD_EXCEPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.actions.addException', { - defaultMessage: 'Add exception', + defaultMessage: 'Add rule exception', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx index 20470d7bb924ff..a3db8fe659d848 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx @@ -96,7 +96,7 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAssociatedToEndpointListLabel', { - defaultMessage: 'Associate rule to Global Endpoint Exception List', + defaultMessage: 'Add existing Endpoint exceptions to the rule', } ), labelAppend: OptionalFieldLabel, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts index 939747717385c4..f4d90d0596ede3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts @@ -30,7 +30,7 @@ export const ADD_FALSE_POSITIVE = i18n.translate( export const GLOBAL_ENDPOINT_EXCEPTION_LIST = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.endpointExceptionListLabel', { - defaultMessage: 'Global endpoint exception list', + defaultMessage: 'Elastic Endpoint exceptions', } ); From 8e0ae5ffd12a6ee05cb782c221b269c2b7198580 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Tue, 4 Aug 2020 10:54:11 -0500 Subject: [PATCH 52/74] add force install option (#74172) --- .../server/routes/epm/handlers.ts | 9 +- .../server/services/epm/packages/install.ts | 11 ++- .../server/types/rest_spec/epm.ts | 5 + .../apis/epm/index.js | 2 +- .../apis/epm/install_errors.ts | 51 ---------- .../apis/epm/install_update.ts | 92 +++++++++++++++++++ .../0.1.0/dataset/test/fields/fields.yml | 0 .../0.1.0/dataset/test/manifest.yml | 0 .../0.1.0/docs/README.md | 0 .../0.1.0/manifest.yml | 6 +- .../0.2.0/dataset/test/fields/fields.yml | 0 .../0.2.0/dataset/test/manifest.yml | 0 .../0.2.0/docs/README.md | 0 .../0.2.0/manifest.yml | 6 +- .../0.3.0/dataset/test/fields/fields.yml | 16 ++++ .../0.3.0/dataset/test/manifest.yml | 9 ++ .../multiple_versions/0.3.0/docs/README.md | 3 + .../multiple_versions/0.3.0/manifest.yml | 20 ++++ 18 files changed, 166 insertions(+), 64 deletions(-) delete mode 100644 x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts create mode 100644 x-pack/test/ingest_manager_api_integration/apis/epm/install_update.ts rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{update => multiple_versions}/0.1.0/dataset/test/fields/fields.yml (100%) rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{update => multiple_versions}/0.1.0/dataset/test/manifest.yml (100%) rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{update => multiple_versions}/0.1.0/docs/README.md (100%) rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{update => multiple_versions}/0.1.0/manifest.yml (67%) rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{update => multiple_versions}/0.2.0/dataset/test/fields/fields.yml (100%) rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{update => multiple_versions}/0.2.0/dataset/test/manifest.yml (100%) rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{update => multiple_versions}/0.2.0/docs/README.md (100%) rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{update => multiple_versions}/0.2.0/manifest.yml (67%) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/docs/README.md create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/manifest.yml diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index f47234fb201182..f8f39f62942600 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -146,9 +146,11 @@ export const getInfoHandler: RequestHandler> = async (context, request, response) => { +export const installPackageHandler: RequestHandler< + TypeOf, + undefined, + TypeOf +> = async (context, request, response) => { const logger = appContextService.getLogger(); const savedObjectsClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; @@ -159,6 +161,7 @@ export const installPackageHandler: RequestHandler { - 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('-'); // TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge // and be replaced by getPackageInfo after adjusting for it to not group/use archive assets const latestPackage = await Registry.fetchFindLatestPackage(pkgName); - if (semver.lt(pkgVersion, latestPackage.version)) + if (semver.lt(pkgVersion, latestPackage.version) && !force) throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`); const paths = await Registry.getArchiveInfo(pkgName, pkgVersion); diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts index 08f47a8f1caaa3..191014606f2202 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts @@ -36,6 +36,11 @@ export const InstallPackageRequestSchema = { params: schema.object({ pkgkey: schema.string(), }), + body: schema.nullable( + schema.object({ + force: schema.boolean(), + }) + ), }; export const DeletePackageRequestSchema = { diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js index 92b41ca4102ee5..1582f72dd1cd82 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js @@ -12,6 +12,6 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./ilm')); loadTestFile(require.resolve('./install_overrides')); loadTestFile(require.resolve('./install_remove_assets')); - loadTestFile(require.resolve('./install_errors')); + loadTestFile(require.resolve('./install_update')); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts deleted file mode 100644 index 8acb11b00b57d3..00000000000000 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install_errors.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; -import { skipIfNoDockerRegistry } from '../../helpers'; - -export default function (providerContext: FtrProviderContext) { - const { getService } = providerContext; - const kibanaServer = getService('kibanaServer'); - const supertest = getService('supertest'); - - describe('package error handling', async () => { - skipIfNoDockerRegistry(providerContext); - it('should return 404 if package does not exist', async function () { - await supertest - .post(`/api/ingest_manager/epm/packages/nonexistent-0.1.0`) - .set('kbn-xsrf', 'xxxx') - .expect(404); - let res; - try { - res = await kibanaServer.savedObjects.get({ - type: 'epm-package', - id: 'nonexistent', - }); - } catch (err) { - res = err; - } - expect(res.response.data.statusCode).equal(404); - }); - it('should return 400 if trying to update/install to an out-of-date package', async function () { - await supertest - .post(`/api/ingest_manager/epm/packages/update-0.1.0`) - .set('kbn-xsrf', 'xxxx') - .expect(400); - let res; - try { - res = await kibanaServer.savedObjects.get({ - type: 'epm-package', - id: 'update', - }); - } catch (err) { - res = err; - } - expect(res.response.data.statusCode).equal(404); - }); - }); -} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_update.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_update.ts new file mode 100644 index 00000000000000..9de6cd9118fe41 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_update.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const kibanaServer = getService('kibanaServer'); + const supertest = getService('supertest'); + + const deletePackage = async (pkgkey: string) => { + await supertest.delete(`/api/ingest_manager/epm/packages/${pkgkey}`).set('kbn-xsrf', 'xxxx'); + }; + + describe('installing and updating scenarios', async () => { + skipIfNoDockerRegistry(providerContext); + after(async () => { + await deletePackage('multiple_versions-0.3.0'); + }); + + it('should return 404 if package does not exist', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/nonexistent-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .expect(404); + let res; + try { + res = await kibanaServer.savedObjects.get({ + type: 'epm-package', + id: 'nonexistent', + }); + } catch (err) { + res = err; + } + expect(res.response.data.statusCode).equal(404); + }); + it('should return 400 if trying to install an out-of-date package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + let res; + try { + res = await kibanaServer.savedObjects.get({ + type: 'epm-package', + id: 'update', + }); + } catch (err) { + res = err; + } + expect(res.response.data.statusCode).equal(404); + }); + it('should return 200 if trying to force install an out-of-date package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + it('should return 400 if trying to update to an out-of-date package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.2.0`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + it('should return 200 if trying to force update to an out-of-date package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.2.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + it('should return 200 if trying to update to the latest package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.3.0`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + await deletePackage('multiple_versions-0.3.0'); + }); + it('should return 200 if trying to install the latest package', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.3.0`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/dataset/test/fields/fields.yml similarity index 100% rename from x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/fields/fields.yml rename to x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/dataset/test/fields/fields.yml diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/dataset/test/manifest.yml similarity index 100% rename from x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/dataset/test/manifest.yml rename to x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/dataset/test/manifest.yml diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/docs/README.md similarity index 100% rename from x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/docs/README.md rename to x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/docs/README.md diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/manifest.yml similarity index 67% rename from x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml rename to x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/manifest.yml index b12f1bfbd3b7ed..32c626b1157390 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.1.0/manifest.yml +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/manifest.yml @@ -1,7 +1,7 @@ format_version: 1.0.0 -name: update -title: Package update test -description: This is a test package for updating a package +name: multiple_versions +title: Package install/update test +description: This is a test package for installing or updating a package version: 0.1.0 categories: [] release: beta diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/dataset/test/fields/fields.yml similarity index 100% rename from x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/fields/fields.yml rename to x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/dataset/test/fields/fields.yml diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/dataset/test/manifest.yml similarity index 100% rename from x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/dataset/test/manifest.yml rename to x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/dataset/test/manifest.yml diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/docs/README.md similarity index 100% rename from x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/docs/README.md rename to x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/docs/README.md diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/manifest.yml similarity index 67% rename from x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml rename to x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/manifest.yml index 11dbdc102dce83..773903a69e7f7c 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/update/0.2.0/manifest.yml +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/manifest.yml @@ -1,7 +1,7 @@ format_version: 1.0.0 -name: update -title: Package update test -description: This is a test package for updating a package +name: multiple_versions +title: Package install/update test +description: This is a test package for installing or updating a packagee version: 0.2.0 categories: [] release: beta diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/fields/fields.yml new file mode 100644 index 00000000000000..12a9a03c1337b4 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/fields/fields.yml @@ -0,0 +1,16 @@ +- name: dataset.type + type: constant_keyword + description: > + Dataset type. +- name: dataset.name + type: constant_keyword + description: > + Dataset name. +- name: dataset.namespace + type: constant_keyword + description: > + Dataset namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/manifest.yml new file mode 100644 index 00000000000000..9ac3c68a0be9ec --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/dataset/test/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/docs/README.md new file mode 100644 index 00000000000000..8e26522d868392 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing installing or updating to an out-of-date package diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/manifest.yml new file mode 100644 index 00000000000000..49c85994d2c2cd --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: multiple_versions +title: Package install/update test +description: This is a test package for installing or updating a package +version: 0.3.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' From b42f64d1a212cf038da9bd77b19bb05c1bc3320a Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Tue, 4 Aug 2020 16:57:53 +0100 Subject: [PATCH 53/74] [Security Solution] Load prepackage timelines by default (#74209) * load prepackage timelines by default * fix unit tests --- .../components/open_timeline/index.test.tsx | 18 +++++++- .../open_timeline_modal/index.test.tsx | 41 ++++++++++++++++++- .../open_timeline/use_timeline_status.tsx | 2 +- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index 43fd57fcfc5bfa..facdc392ff7ba2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx @@ -25,11 +25,10 @@ import { getTimelineTabsUrl } from '../../../common/components/link_to'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; import { useGetAllTimeline, getAllTimeline } from '../../containers/all'; +import { useTimelineStatus } from './use_timeline_status'; import { NotePreviews } from './note_previews'; import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; - import { StatefulOpenTimeline } from '.'; - import { TimelineTabsStyle } from './types'; import { useTimelineTypes, @@ -75,9 +74,17 @@ jest.mock('../../../common/components/link_to', () => { }; }); +jest.mock('./use_timeline_status', () => { + return { + useTimelineStatus: jest.fn(), + }; +}); + describe('StatefulOpenTimeline', () => { const title = 'All Timelines / Open Timelines'; let mockHistory: History[]; + const mockInstallPrepackagedTimelines = jest.fn(); + beforeEach(() => { (useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.default, @@ -95,6 +102,13 @@ describe('StatefulOpenTimeline', () => { totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount, refetch: jest.fn(), }); + ((useTimelineStatus as unknown) as jest.Mock).mockReturnValue({ + timelineStatus: null, + templateTimelineType: null, + templateTimelineFilter:
, + installPrepackagedTimelines: mockInstallPrepackagedTimelines, + }); + mockInstallPrepackagedTimelines.mockClear(); }); afterEach(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx index 3017f553d59d5f..5ce53607817ebb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx @@ -12,10 +12,11 @@ import { ThemeProvider } from 'styled-components'; // we don't have the types for waitFor just yet, so using "as waitFor" until when we do import { wait as waitFor } from '@testing-library/react'; + import { TestProviderWithoutDragAndDrop } from '../../../../common/mock/test_providers'; import { mockOpenTimelineQueryResults } from '../../../../common/mock/timeline_results'; import { useGetAllTimeline, getAllTimeline } from '../../../containers/all'; - +import { useTimelineStatus } from '../use_timeline_status'; import { OpenTimelineModal } from '.'; jest.mock('../../../../common/lib/kibana'); @@ -39,8 +40,15 @@ jest.mock('../use_timeline_types', () => { }; }); +jest.mock('../use_timeline_status', () => { + return { + useTimelineStatus: jest.fn(), + }; +}); + describe('OpenTimelineModal', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const mockInstallPrepackagedTimelines = jest.fn(); beforeEach(() => { ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ fetchAllTimeline: jest.fn(), @@ -52,6 +60,16 @@ describe('OpenTimelineModal', () => { totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount, refetch: jest.fn(), }); + ((useTimelineStatus as unknown) as jest.Mock).mockReturnValue({ + timelineStatus: null, + templateTimelineType: null, + templateTimelineFilter:
, + installPrepackagedTimelines: mockInstallPrepackagedTimelines, + }); + }); + + afterEach(() => { + mockInstallPrepackagedTimelines.mockClear(); }); test('it renders the expected modal', async () => { @@ -76,4 +94,25 @@ describe('OpenTimelineModal', () => { { timeout: 10000 } ); }, 20000); + + test('it installs elastic prebuilt templates', async () => { + const wrapper = mount( + + + + + + + + ); + + await waitFor( + () => { + wrapper.update(); + + expect(mockInstallPrepackagedTimelines).toHaveBeenCalled(); + }, + { timeout: 10000 } + ); + }, 20000); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx index 8c4c686698c884..37bea3d713c088 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx @@ -102,7 +102,7 @@ export const useTimelineStatus = ({ }, [templateTimelineType, filters, isTemplateFilterEnabled, onFilterClicked]); const installPrepackagedTimelines = useCallback(async () => { - if (templateTimelineType === TemplateTimelineType.elastic) { + if (templateTimelineType !== TemplateTimelineType.custom) { await installPrepackedTimelines(); } }, [templateTimelineType]); From 89dba3927333fc2f6b60573a1e173d966510ee41 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Tue, 4 Aug 2020 17:13:32 +0100 Subject: [PATCH 54/74] Handle modifier keys (#74237) --- x-pack/plugins/infra/public/hooks/use_link_props.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugins/infra/public/hooks/use_link_props.tsx b/x-pack/plugins/infra/public/hooks/use_link_props.tsx index dec8eaae56f41f..710011d3bdf3ea 100644 --- a/x-pack/plugins/infra/public/hooks/use_link_props.tsx +++ b/x-pack/plugins/infra/public/hooks/use_link_props.tsx @@ -68,6 +68,9 @@ export const useLinkProps = ( const onClick = useMemo(() => { return (e: React.MouseEvent | React.MouseEvent) => { + if (e.defaultPrevented || isModifiedEvent(e)) { + return; + } e.preventDefault(); const navigate = () => { @@ -112,3 +115,6 @@ const validateParams = ({ app, pathname, hash, search }: LinkDescriptor) => { ); } }; + +const isModifiedEvent = (event: any) => + !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); From 2dea17a8d0eff4730335b776885699ee946548e9 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 4 Aug 2020 12:54:20 -0400 Subject: [PATCH 55/74] Using msearch for tree api endpoint (#73813) --- .../server/endpoint/routes/resolver/tree.ts | 40 +++++-------------- .../utils/children_lifecycle_query_handler.ts | 2 +- .../endpoint/routes/resolver/utils/fetch.ts | 2 +- .../apis/index.ts | 4 +- .../apis/resolver/entity_id.ts | 2 +- .../apis/resolver/index.ts | 16 ++++++++ .../apis/resolver/tree.ts | 2 +- 7 files changed, 30 insertions(+), 38 deletions(-) create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts index 33011078ee8233..02cddc3ddcf6ef 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts @@ -9,7 +9,6 @@ import { TypeOf } from '@kbn/config-schema'; import { eventsIndexPattern, alertsIndexPattern } from '../../../../common/endpoint/constants'; import { validateTree } from '../../../../common/endpoint/schema/resolver'; import { Fetcher } from './utils/fetch'; -import { Tree } from './utils/tree'; import { EndpointAppContext } from '../../types'; export function handleTree( @@ -17,42 +16,21 @@ export function handleTree( endpointAppContext: EndpointAppContext ): RequestHandler, TypeOf> { return async (context, req, res) => { - const { - params: { id }, - query: { - children, - ancestors, - events, - alerts, - afterAlert, - afterEvent, - afterChild, - legacyEndpointID: endpointID, - }, - } = req; try { const client = context.core.elasticsearch.legacy.client; - const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID); + const fetcher = new Fetcher( + client, + req.params.id, + eventsIndexPattern, + alertsIndexPattern, + req.query.legacyEndpointID + ); - const [childrenNodes, ancestry, relatedEvents, relatedAlerts] = await Promise.all([ - fetcher.children(children, afterChild), - fetcher.ancestors(ancestors), - fetcher.events(events, afterEvent), - fetcher.alerts(alerts, afterAlert), - ]); - - const tree = new Tree(id, { - ancestry, - children: childrenNodes, - relatedEvents, - relatedAlerts, - }); - - const enrichedTree = await fetcher.stats(tree); + const tree = await fetcher.tree(req.query); return res.ok({ - body: enrichedTree.render(), + body: tree.render(), }); } catch (err) { log.warn(err); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts index 8aaf809405d631..ab610dc9776caa 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts @@ -66,6 +66,6 @@ export class ChildrenLifecycleQueryHandler implements SingleQueryHandler { await ingestManager.setup(); }); - loadTestFile(require.resolve('./resolver/entity_id')); - loadTestFile(require.resolve('./resolver/tree')); - loadTestFile(require.resolve('./resolver/children')); + loadTestFile(require.resolve('./resolver/index')); loadTestFile(require.resolve('./metadata')); loadTestFile(require.resolve('./policy')); loadTestFile(require.resolve('./artifacts')); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts index 231871fae3d39a..cb6c49e17c7125 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts @@ -16,7 +16,7 @@ import { } from '../../../../plugins/security_solution/common/endpoint/generate_data'; import { InsertedEvents } from '../../services/resolver'; -export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { +export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const resolver = getService('resolverGenerator'); const generator = new EndpointDocGenerator('resolver'); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts new file mode 100644 index 00000000000000..dc9a1fab9ec020 --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function (providerContext: FtrProviderContext) { + const { loadTestFile } = providerContext; + + describe('Resolver tests', () => { + loadTestFile(require.resolve('./entity_id')); + loadTestFile(require.resolve('./children')); + loadTestFile(require.resolve('./tree')); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts index 758e26fd5eeced..7b511c3be74b54 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts @@ -230,7 +230,7 @@ const verifyLifecycleStats = ( } }; -export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { +export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const resolver = getService('resolverGenerator'); From 0ca8e99a9f62435e05f6af0b9af2586a3b02b3cd Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 4 Aug 2020 13:13:51 -0400 Subject: [PATCH 56/74] [CI] Add pipeline task queue framework and merge workers into one (#71268) --- .ci/Dockerfile | 38 +++ .ci/runbld_no_junit.yml | 2 +- .gitignore | 2 + Jenkinsfile | 44 +--- packages/kbn-dev-utils/src/run/help.test.ts | 6 +- src/dev/ci_setup/checkout_sibling_es.sh | 12 +- src/dev/ci_setup/setup_env.sh | 4 +- src/dev/notice/generate_notice_from_source.ts | 2 + tasks/test_jest.js | 6 +- test/scripts/checks/doc_api_changes.sh | 5 + test/scripts/checks/file_casing.sh | 5 + test/scripts/checks/i18n.sh | 5 + test/scripts/checks/licenses.sh | 5 + test/scripts/checks/lock_file_symlinks.sh | 5 + test/scripts/checks/telemetry.sh | 5 + test/scripts/checks/test_hardening.sh | 5 + test/scripts/checks/test_projects.sh | 5 + test/scripts/checks/ts_projects.sh | 5 + test/scripts/checks/type_check.sh | 5 + .../checks/verify_dependency_versions.sh | 5 + test/scripts/checks/verify_notice.sh | 5 + test/scripts/jenkins_accessibility.sh | 2 +- .../jenkins_build_kbn_sample_panel_action.sh | 0 test/scripts/jenkins_build_kibana.sh | 13 +- test/scripts/jenkins_build_plugins.sh | 12 + test/scripts/jenkins_ci_group.sh | 2 +- test/scripts/jenkins_firefox_smoke.sh | 2 +- test/scripts/jenkins_plugin_functional.sh | 15 ++ .../jenkins_security_solution_cypress.sh | 8 +- .../jenkins_setup_parallel_workspace.sh | 32 +++ test/scripts/jenkins_test_setup.sh | 6 + test/scripts/jenkins_test_setup_oss.sh | 15 +- test/scripts/jenkins_test_setup_xpack.sh | 15 +- test/scripts/jenkins_xpack_accessibility.sh | 2 +- test/scripts/jenkins_xpack_build_kibana.sh | 17 +- test/scripts/jenkins_xpack_build_plugins.sh | 14 + ...nkins_xpack_saved_objects_field_metrics.sh | 2 +- test/scripts/lint/eslint.sh | 5 + test/scripts/lint/sasslint.sh | 5 + test/scripts/test/api_integration.sh | 5 + test/scripts/test/jest_integration.sh | 5 + test/scripts/test/jest_unit.sh | 5 + test/scripts/test/karma_ci.sh | 5 + test/scripts/test/mocha.sh | 5 + test/scripts/test/safer_lodash_set.sh | 5 + test/scripts/test/xpack_jest_unit.sh | 6 + test/scripts/test/xpack_karma.sh | 6 + .../test/xpack_list_cyclic_dependency.sh | 6 + .../test/xpack_siem_cyclic_dependency.sh | 6 + vars/catchErrors.groovy | 11 +- vars/kibanaPipeline.groovy | 241 +++++++++++++++--- vars/task.groovy | 5 + vars/tasks.groovy | 119 +++++++++ vars/withTaskQueue.groovy | 154 +++++++++++ vars/workers.groovy | 12 +- .../canvas/storybook/storyshots.test.tsx | 7 + 56 files changed, 815 insertions(+), 131 deletions(-) create mode 100644 .ci/Dockerfile create mode 100755 test/scripts/checks/doc_api_changes.sh create mode 100755 test/scripts/checks/file_casing.sh create mode 100755 test/scripts/checks/i18n.sh create mode 100755 test/scripts/checks/licenses.sh create mode 100755 test/scripts/checks/lock_file_symlinks.sh create mode 100755 test/scripts/checks/telemetry.sh create mode 100755 test/scripts/checks/test_hardening.sh create mode 100755 test/scripts/checks/test_projects.sh create mode 100755 test/scripts/checks/ts_projects.sh create mode 100755 test/scripts/checks/type_check.sh create mode 100755 test/scripts/checks/verify_dependency_versions.sh create mode 100755 test/scripts/checks/verify_notice.sh mode change 100644 => 100755 test/scripts/jenkins_build_kbn_sample_panel_action.sh create mode 100755 test/scripts/jenkins_build_plugins.sh create mode 100755 test/scripts/jenkins_plugin_functional.sh mode change 100644 => 100755 test/scripts/jenkins_security_solution_cypress.sh create mode 100755 test/scripts/jenkins_setup_parallel_workspace.sh mode change 100644 => 100755 test/scripts/jenkins_test_setup.sh mode change 100644 => 100755 test/scripts/jenkins_test_setup_oss.sh mode change 100644 => 100755 test/scripts/jenkins_test_setup_xpack.sh create mode 100755 test/scripts/jenkins_xpack_build_plugins.sh create mode 100755 test/scripts/lint/eslint.sh create mode 100755 test/scripts/lint/sasslint.sh create mode 100755 test/scripts/test/api_integration.sh create mode 100755 test/scripts/test/jest_integration.sh create mode 100755 test/scripts/test/jest_unit.sh create mode 100755 test/scripts/test/karma_ci.sh create mode 100755 test/scripts/test/mocha.sh create mode 100755 test/scripts/test/safer_lodash_set.sh create mode 100755 test/scripts/test/xpack_jest_unit.sh create mode 100755 test/scripts/test/xpack_karma.sh create mode 100755 test/scripts/test/xpack_list_cyclic_dependency.sh create mode 100755 test/scripts/test/xpack_siem_cyclic_dependency.sh create mode 100644 vars/task.groovy create mode 100644 vars/tasks.groovy create mode 100644 vars/withTaskQueue.groovy diff --git a/.ci/Dockerfile b/.ci/Dockerfile new file mode 100644 index 00000000000000..d90d9f4710b5b9 --- /dev/null +++ b/.ci/Dockerfile @@ -0,0 +1,38 @@ +# NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. +# If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts + +ARG NODE_VERSION=10.21.0 + +FROM node:${NODE_VERSION} AS base + +RUN apt-get update && \ + apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \ + libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \ + libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \ + libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \ + libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget openjdk-8-jre && \ + rm -rf /var/lib/apt/lists/* + +RUN curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ + && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ + && apt-get update \ + && apt-get install -y rsync jq bsdtar google-chrome-stable \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +RUN LATEST_VAULT_RELEASE=$(curl -s https://api.github.com/repos/hashicorp/vault/tags | jq --raw-output .[0].name[1:]) \ + && curl -L https://releases.hashicorp.com/vault/${LATEST_VAULT_RELEASE}/vault_${LATEST_VAULT_RELEASE}_linux_amd64.zip -o vault.zip \ + && unzip vault.zip \ + && rm vault.zip \ + && chmod +x vault \ + && mv vault /usr/local/bin/vault + +RUN groupadd -r kibana && useradd -r -g kibana kibana && mkdir /home/kibana && chown kibana:kibana /home/kibana + +COPY ./bash_standard_lib.sh /usr/local/bin/bash_standard_lib.sh +RUN chmod +x /usr/local/bin/bash_standard_lib.sh + +COPY ./runbld /usr/local/bin/runbld +RUN chmod +x /usr/local/bin/runbld + +USER kibana diff --git a/.ci/runbld_no_junit.yml b/.ci/runbld_no_junit.yml index 67b5002c1c4377..1bcb7e22a26480 100644 --- a/.ci/runbld_no_junit.yml +++ b/.ci/runbld_no_junit.yml @@ -3,4 +3,4 @@ profiles: - ".*": # Match any job tests: - junit-filename-pattern: "8d8bd494-d909-4e67-a052-7e8b5aaeb5e4" # A bogus path that should never exist + junit-filename-pattern: false diff --git a/.gitignore b/.gitignore index dfd02de7b11862..1d12ef2a9cff3a 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,8 @@ npm-debug.log* .tern-project .nyc_output .ci/pipeline-library/build/ +.ci/runbld +.ci/bash_standard_lib.sh .gradle # apm plugin diff --git a/Jenkinsfile b/Jenkinsfile index ad1d244c788740..3b68cde206573a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,49 +9,7 @@ kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true, setCommitStatus: true) ciStats.trackBuild { catchError { retryable.enable() - parallel([ - 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), - 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), - 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), - 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), - 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), - 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), - 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), - 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), - 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), - 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), - 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), - 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), - 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), - 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), - 'oss-accessibility': kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh'), - // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'), - ]), - 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), - 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), - 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), - 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), - 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), - 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), - 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), - 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), - 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), - 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), - 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), - 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), - 'xpack-savedObjectsFieldMetrics': kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh'), - 'xpack-securitySolutionCypress': { processNumber -> - whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/', 'x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/', 'x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx']) { - kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')(processNumber) - } - }, - - // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), - ]), - ]) + kibanaPipeline.allCiTasks() } } } diff --git a/packages/kbn-dev-utils/src/run/help.test.ts b/packages/kbn-dev-utils/src/run/help.test.ts index 27be7ad28b81a6..300f1cba7eb7d4 100644 --- a/packages/kbn-dev-utils/src/run/help.test.ts +++ b/packages/kbn-dev-utils/src/run/help.test.ts @@ -57,7 +57,7 @@ const barCommand: Command = { usage: 'bar [...names]', }; -describe('getHelp()', () => { +describe.skip('getHelp()', () => { it('returns the expected output', () => { expect( getHelp({ @@ -95,7 +95,7 @@ describe('getHelp()', () => { }); }); -describe('getCommandLevelHelp()', () => { +describe.skip('getCommandLevelHelp()', () => { it('returns the expected output', () => { expect( getCommandLevelHelp({ @@ -141,7 +141,7 @@ describe('getCommandLevelHelp()', () => { }); }); -describe('getHelpForAllCommands()', () => { +describe.skip('getHelpForAllCommands()', () => { it('returns the expected output', () => { expect( getHelpForAllCommands({ diff --git a/src/dev/ci_setup/checkout_sibling_es.sh b/src/dev/ci_setup/checkout_sibling_es.sh index 915759d4214f9a..3832ec9b4076a7 100755 --- a/src/dev/ci_setup/checkout_sibling_es.sh +++ b/src/dev/ci_setup/checkout_sibling_es.sh @@ -7,10 +7,11 @@ function checkout_sibling { targetDir=$2 useExistingParamName=$3 useExisting="$(eval "echo "\$$useExistingParamName"")" + repoAddress="https://github.com/" if [ -z ${useExisting:+x} ]; then if [ -d "$targetDir" ]; then - echo "I expected a clean workspace but an '${project}' sibling directory already exists in [$PARENT_DIR]!" + echo "I expected a clean workspace but an '${project}' sibling directory already exists in [$WORKSPACE]!" echo echo "Either define '${useExistingParamName}' or remove the existing '${project}' sibling." exit 1 @@ -21,8 +22,9 @@ function checkout_sibling { cloneBranch="" function clone_target_is_valid { + echo " -> checking for '${cloneBranch}' branch at ${cloneAuthor}/${project}" - if [[ -n "$(git ls-remote --heads "git@github.com:${cloneAuthor}/${project}.git" ${cloneBranch} 2>/dev/null)" ]]; then + if [[ -n "$(git ls-remote --heads "${repoAddress}${cloneAuthor}/${project}.git" ${cloneBranch} 2>/dev/null)" ]]; then return 0 else return 1 @@ -71,7 +73,7 @@ function checkout_sibling { fi echo " -> checking out '${cloneBranch}' branch from ${cloneAuthor}/${project}..." - git clone -b "$cloneBranch" "git@github.com:${cloneAuthor}/${project}.git" "$targetDir" --depth=1 + git clone -b "$cloneBranch" "${repoAddress}${cloneAuthor}/${project}.git" "$targetDir" --depth=1 echo " -> checked out ${project} revision: $(git -C "${targetDir}" rev-parse HEAD)" echo } @@ -87,12 +89,12 @@ function checkout_sibling { fi } -checkout_sibling "elasticsearch" "${PARENT_DIR}/elasticsearch" "USE_EXISTING_ES" +checkout_sibling "elasticsearch" "${WORKSPACE}/elasticsearch" "USE_EXISTING_ES" export TEST_ES_FROM=${TEST_ES_FROM:-snapshot} # Set the JAVA_HOME based on the Java property file in the ES repo # This assumes the naming convention used on CI (ex: ~/.java/java10) -ES_DIR="$PARENT_DIR/elasticsearch" +ES_DIR="$WORKSPACE/elasticsearch" ES_JAVA_PROP_PATH=$ES_DIR/.ci/java-versions.properties diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 86927b694679a4..72ec73ad810e6d 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -53,6 +53,8 @@ export PARENT_DIR="$parentDir" kbnBranch="$(jq -r .branch "$KIBANA_DIR/package.json")" export KIBANA_PKG_BRANCH="$kbnBranch" +export WORKSPACE="${WORKSPACE:-$PARENT_DIR}" + ### ### download node ### @@ -162,7 +164,7 @@ export -f checks-reporter-with-killswitch source "$KIBANA_DIR/src/dev/ci_setup/load_env_keys.sh" -ES_DIR="$PARENT_DIR/elasticsearch" +ES_DIR="$WORKSPACE/elasticsearch" ES_JAVA_PROP_PATH=$ES_DIR/.ci/java-versions.properties if [[ -d "$ES_DIR" && -f "$ES_JAVA_PROP_PATH" ]]; then diff --git a/src/dev/notice/generate_notice_from_source.ts b/src/dev/notice/generate_notice_from_source.ts index 37bbcce72e4974..0bef5bc5f32d42 100644 --- a/src/dev/notice/generate_notice_from_source.ts +++ b/src/dev/notice/generate_notice_from_source.ts @@ -49,8 +49,10 @@ export async function generateNoticeFromSource({ productName, directory, log }: ignore: [ '{node_modules,build,dist,data,built_assets}/**', 'packages/*/{node_modules,build,dist}/**', + 'src/plugins/*/{node_modules,build,dist}/**', 'x-pack/{node_modules,build,dist,data}/**', 'x-pack/packages/*/{node_modules,build,dist}/**', + 'x-pack/plugins/*/{node_modules,build,dist}/**', '**/target/**', ], }; diff --git a/tasks/test_jest.js b/tasks/test_jest.js index d8f51806e8ddc8..810ed423248400 100644 --- a/tasks/test_jest.js +++ b/tasks/test_jest.js @@ -22,7 +22,7 @@ const { resolve } = require('path'); module.exports = function (grunt) { grunt.registerTask('test:jest', function () { const done = this.async(); - runJest(resolve(__dirname, '../scripts/jest.js')).then(done, done); + runJest(resolve(__dirname, '../scripts/jest.js'), ['--maxWorkers=10']).then(done, done); }); grunt.registerTask('test:jest_integration', function () { @@ -30,10 +30,10 @@ module.exports = function (grunt) { runJest(resolve(__dirname, '../scripts/jest_integration.js')).then(done, done); }); - function runJest(jestScript) { + function runJest(jestScript, args = []) { const serverCmd = { cmd: 'node', - args: [jestScript, '--ci'], + args: [jestScript, '--ci', ...args], opts: { stdio: 'inherit' }, }; diff --git a/test/scripts/checks/doc_api_changes.sh b/test/scripts/checks/doc_api_changes.sh new file mode 100755 index 00000000000000..503d12b2f6d73d --- /dev/null +++ b/test/scripts/checks/doc_api_changes.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:checkDocApiChanges diff --git a/test/scripts/checks/file_casing.sh b/test/scripts/checks/file_casing.sh new file mode 100755 index 00000000000000..513664263791b9 --- /dev/null +++ b/test/scripts/checks/file_casing.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:checkFileCasing diff --git a/test/scripts/checks/i18n.sh b/test/scripts/checks/i18n.sh new file mode 100755 index 00000000000000..7a6fd46c46c769 --- /dev/null +++ b/test/scripts/checks/i18n.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:i18nCheck diff --git a/test/scripts/checks/licenses.sh b/test/scripts/checks/licenses.sh new file mode 100755 index 00000000000000..a08d7d07a24a13 --- /dev/null +++ b/test/scripts/checks/licenses.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:licenses diff --git a/test/scripts/checks/lock_file_symlinks.sh b/test/scripts/checks/lock_file_symlinks.sh new file mode 100755 index 00000000000000..1d43d32c9feb82 --- /dev/null +++ b/test/scripts/checks/lock_file_symlinks.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:checkLockfileSymlinks diff --git a/test/scripts/checks/telemetry.sh b/test/scripts/checks/telemetry.sh new file mode 100755 index 00000000000000..c74ec295b385ce --- /dev/null +++ b/test/scripts/checks/telemetry.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:telemetryCheck diff --git a/test/scripts/checks/test_hardening.sh b/test/scripts/checks/test_hardening.sh new file mode 100755 index 00000000000000..9184758577654a --- /dev/null +++ b/test/scripts/checks/test_hardening.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_hardening diff --git a/test/scripts/checks/test_projects.sh b/test/scripts/checks/test_projects.sh new file mode 100755 index 00000000000000..5f9aafe80e10e6 --- /dev/null +++ b/test/scripts/checks/test_projects.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_projects diff --git a/test/scripts/checks/ts_projects.sh b/test/scripts/checks/ts_projects.sh new file mode 100755 index 00000000000000..d667c753baec23 --- /dev/null +++ b/test/scripts/checks/ts_projects.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:checkTsProjects diff --git a/test/scripts/checks/type_check.sh b/test/scripts/checks/type_check.sh new file mode 100755 index 00000000000000..07c49638134be1 --- /dev/null +++ b/test/scripts/checks/type_check.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:typeCheck diff --git a/test/scripts/checks/verify_dependency_versions.sh b/test/scripts/checks/verify_dependency_versions.sh new file mode 100755 index 00000000000000..b73a71e7ff7fd5 --- /dev/null +++ b/test/scripts/checks/verify_dependency_versions.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:verifyDependencyVersions diff --git a/test/scripts/checks/verify_notice.sh b/test/scripts/checks/verify_notice.sh new file mode 100755 index 00000000000000..9f8343e5408615 --- /dev/null +++ b/test/scripts/checks/verify_notice.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:verifyNotice diff --git a/test/scripts/jenkins_accessibility.sh b/test/scripts/jenkins_accessibility.sh index c122d71b58edb4..fa7cbd41d70785 100755 --- a/test/scripts/jenkins_accessibility.sh +++ b/test/scripts/jenkins_accessibility.sh @@ -5,5 +5,5 @@ source test/scripts/jenkins_test_setup_oss.sh checks-reporter-with-killswitch "Kibana accessibility tests" \ node scripts/functional_tests \ --debug --bail \ - --kibana-install-dir "$installDir" \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ --config test/accessibility/config.ts; diff --git a/test/scripts/jenkins_build_kbn_sample_panel_action.sh b/test/scripts/jenkins_build_kbn_sample_panel_action.sh old mode 100644 new mode 100755 diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index 2310a35f94f331..f449986713f97d 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -2,13 +2,9 @@ source src/dev/ci_setup/setup_env.sh -echo " -> building kibana platform plugins" -node scripts/build_kibana_platform_plugins \ - --oss \ - --filter '!alertingExample' \ - --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ - --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ - --verbose; +if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then + ./test/scripts/jenkins_build_plugins.sh +fi # doesn't persist, also set in kibanaPipeline.groovy export KBN_NP_PLUGINS_BUILT=true @@ -20,4 +16,7 @@ yarn run grunt functionalTests:ensureAllTestsInCiGroup; if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> building and extracting OSS Kibana distributable for use in functional tests" node scripts/build --debug --oss + + mkdir -p "$WORKSPACE/kibana-build-oss" + cp -pR build/oss/kibana-*-SNAPSHOT-linux-x86_64/. $WORKSPACE/kibana-build-oss/ fi diff --git a/test/scripts/jenkins_build_plugins.sh b/test/scripts/jenkins_build_plugins.sh new file mode 100755 index 00000000000000..0c3ee4e3f261fb --- /dev/null +++ b/test/scripts/jenkins_build_plugins.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +echo " -> building kibana platform plugins" +node scripts/build_kibana_platform_plugins \ + --oss \ + --filter '!alertingExample' \ + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ + --workers 6 \ + --verbose diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index 60d7f0406f4c9f..2542d7032e83bf 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -5,7 +5,7 @@ source test/scripts/jenkins_test_setup_oss.sh if [[ -z "$CODE_COVERAGE" ]]; then checks-reporter-with-killswitch "Functional tests / Group ${CI_GROUP}" yarn run grunt "run:functionalTests_ciGroup${CI_GROUP}"; - if [ "$CI_GROUP" == "1" ]; then + if [[ ! "$TASK_QUEUE_PROCESS_ID" && "$CI_GROUP" == "1" ]]; then source test/scripts/jenkins_build_kbn_sample_panel_action.sh yarn run grunt run:pluginFunctionalTestsRelease --from=source; yarn run grunt run:exampleFunctionalTestsRelease --from=source; diff --git a/test/scripts/jenkins_firefox_smoke.sh b/test/scripts/jenkins_firefox_smoke.sh index 2bba6e06d76d7e..247ab360b79124 100755 --- a/test/scripts/jenkins_firefox_smoke.sh +++ b/test/scripts/jenkins_firefox_smoke.sh @@ -5,6 +5,6 @@ source test/scripts/jenkins_test_setup_oss.sh checks-reporter-with-killswitch "Firefox smoke test" \ node scripts/functional_tests \ --bail --debug \ - --kibana-install-dir "$installDir" \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ --include-tag "includeFirefox" \ --config test/functional/config.firefox.js; diff --git a/test/scripts/jenkins_plugin_functional.sh b/test/scripts/jenkins_plugin_functional.sh new file mode 100755 index 00000000000000..1d691d98982dea --- /dev/null +++ b/test/scripts/jenkins_plugin_functional.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_oss.sh + +cd test/plugin_functional/plugins/kbn_sample_panel_action; +if [[ ! -d "target" ]]; then + yarn build; +fi +cd -; + +pwd + +yarn run grunt run:pluginFunctionalTestsRelease --from=source; +yarn run grunt run:exampleFunctionalTestsRelease --from=source; +yarn run grunt run:interpreterFunctionalTestsRelease; diff --git a/test/scripts/jenkins_security_solution_cypress.sh b/test/scripts/jenkins_security_solution_cypress.sh old mode 100644 new mode 100755 index 204911a3eedaa6..a5a1a2103801fa --- a/test/scripts/jenkins_security_solution_cypress.sh +++ b/test/scripts/jenkins_security_solution_cypress.sh @@ -1,12 +1,6 @@ #!/usr/bin/env bash -source test/scripts/jenkins_test_setup.sh - -installDir="$PARENT_DIR/install/kibana" -destDir="${installDir}-${CI_WORKER_NUMBER}" -cp -R "$installDir" "$destDir" - -export KIBANA_INSTALL_DIR="$destDir" +source test/scripts/jenkins_test_setup_xpack.sh echo " -> Running security solution cypress tests" cd "$XPACK_DIR" diff --git a/test/scripts/jenkins_setup_parallel_workspace.sh b/test/scripts/jenkins_setup_parallel_workspace.sh new file mode 100755 index 00000000000000..5274d05572e713 --- /dev/null +++ b/test/scripts/jenkins_setup_parallel_workspace.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -e + +CURRENT_DIR=$(pwd) + +# Copy everything except node_modules into the current workspace +rsync -a ${WORKSPACE}/kibana/* . --exclude node_modules +rsync -a ${WORKSPACE}/kibana/.??* . + +# Symlink all non-root, non-fixture node_modules into our new workspace +cd ${WORKSPACE}/kibana +find . -type d -name node_modules -not -path '*__fixtures__*' -not -path './node_modules*' -prune -print0 | xargs -0I % ln -s "${WORKSPACE}/kibana/%" "${CURRENT_DIR}/%" +find . -type d -wholename '*__fixtures__*node_modules' -not -path './node_modules*' -prune -print0 | xargs -0I % cp -R "${WORKSPACE}/kibana/%" "${CURRENT_DIR}/%" +cd "${CURRENT_DIR}" + +# Symlink all of the individual root-level node_modules into the node_modules/ directory +mkdir -p node_modules +ln -s ${WORKSPACE}/kibana/node_modules/* node_modules/ +ln -s ${WORKSPACE}/kibana/node_modules/.??* node_modules/ + +# Copy a few node_modules instead of symlinking them. They don't work correctly if symlinked +unlink node_modules/@kbn +unlink node_modules/css-loader +unlink node_modules/style-loader + +# packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts will fail if this is a symlink +unlink node_modules/val-loader + +cp -R ${WORKSPACE}/kibana/node_modules/@kbn node_modules/ +cp -R ${WORKSPACE}/kibana/node_modules/css-loader node_modules/ +cp -R ${WORKSPACE}/kibana/node_modules/style-loader node_modules/ +cp -R ${WORKSPACE}/kibana/node_modules/val-loader node_modules/ diff --git a/test/scripts/jenkins_test_setup.sh b/test/scripts/jenkins_test_setup.sh old mode 100644 new mode 100755 index 49ee8a6b526ca5..05b88aa2dd0a20 --- a/test/scripts/jenkins_test_setup.sh +++ b/test/scripts/jenkins_test_setup.sh @@ -14,3 +14,9 @@ trap 'post_work' EXIT export TEST_BROWSER_HEADLESS=1 source src/dev/ci_setup/setup_env.sh + +# For parallel workspaces, we should copy the .es directory from the root, because it should already have downloaded snapshots in it +# This isn't part of jenkins_setup_parallel_workspace.sh just because not all tasks require ES +if [[ ! -d .es && -d "$WORKSPACE/kibana/.es" ]]; then + cp -R $WORKSPACE/kibana/.es ./ +fi diff --git a/test/scripts/jenkins_test_setup_oss.sh b/test/scripts/jenkins_test_setup_oss.sh old mode 100644 new mode 100755 index 7bbb8675263843..b7eac33f351768 --- a/test/scripts/jenkins_test_setup_oss.sh +++ b/test/scripts/jenkins_test_setup_oss.sh @@ -2,10 +2,17 @@ source test/scripts/jenkins_test_setup.sh -if [[ -z "$CODE_COVERAGE" ]] ; then - installDir="$(realpath $PARENT_DIR/kibana/build/oss/kibana-*-SNAPSHOT-linux-x86_64)" - destDir=${installDir}-${CI_PARALLEL_PROCESS_NUMBER} - cp -R "$installDir" "$destDir" +if [[ -z "$CODE_COVERAGE" ]]; then + + destDir="build/kibana-build-oss" + if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then + destDir="${destDir}-${CI_PARALLEL_PROCESS_NUMBER}" + fi + + if [[ ! -d $destDir ]]; then + mkdir -p $destDir + cp -pR "$WORKSPACE/kibana-build-oss/." $destDir/ + fi export KIBANA_INSTALL_DIR="$destDir" fi diff --git a/test/scripts/jenkins_test_setup_xpack.sh b/test/scripts/jenkins_test_setup_xpack.sh old mode 100644 new mode 100755 index a72e9749ebbd5b..74a3de77e3a760 --- a/test/scripts/jenkins_test_setup_xpack.sh +++ b/test/scripts/jenkins_test_setup_xpack.sh @@ -3,11 +3,18 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]]; then - installDir="$PARENT_DIR/install/kibana" - destDir="${installDir}-${CI_PARALLEL_PROCESS_NUMBER}" - cp -R "$installDir" "$destDir" - export KIBANA_INSTALL_DIR="$destDir" + destDir="build/kibana-build-xpack" + if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then + destDir="${destDir}-${CI_PARALLEL_PROCESS_NUMBER}" + fi + + if [[ ! -d $destDir ]]; then + mkdir -p $destDir + cp -pR "$WORKSPACE/kibana-build-xpack/." $destDir/ + fi + + export KIBANA_INSTALL_DIR="$(realpath $destDir)" cd "$XPACK_DIR" fi diff --git a/test/scripts/jenkins_xpack_accessibility.sh b/test/scripts/jenkins_xpack_accessibility.sh index a3c03dd780886a..3afd4bfb76396d 100755 --- a/test/scripts/jenkins_xpack_accessibility.sh +++ b/test/scripts/jenkins_xpack_accessibility.sh @@ -5,5 +5,5 @@ source test/scripts/jenkins_test_setup_xpack.sh checks-reporter-with-killswitch "X-Pack accessibility tests" \ node scripts/functional_tests \ --debug --bail \ - --kibana-install-dir "$installDir" \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ --config test/accessibility/config.ts; diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh index c962b962b1e5e6..2452e2f5b8c58c 100755 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ b/test/scripts/jenkins_xpack_build_kibana.sh @@ -3,15 +3,9 @@ cd "$KIBANA_DIR" source src/dev/ci_setup/setup_env.sh -echo " -> building kibana platform plugins" -node scripts/build_kibana_platform_plugins \ - --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ - --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ - --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \ - --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \ - --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ - --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \ - --verbose; +if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then + ./test/scripts/jenkins_xpack_build_plugins.sh +fi # doesn't persist, also set in kibanaPipeline.groovy export KBN_NP_PLUGINS_BUILT=true @@ -36,7 +30,10 @@ if [[ -z "$CODE_COVERAGE" ]] ; then cd "$KIBANA_DIR" node scripts/build --debug --no-oss linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" - installDir="$PARENT_DIR/install/kibana" + installDir="$KIBANA_DIR/install/kibana" mkdir -p "$installDir" tar -xzf "$linuxBuild" -C "$installDir" --strip=1 + + mkdir -p "$WORKSPACE/kibana-build-xpack" + cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/ fi diff --git a/test/scripts/jenkins_xpack_build_plugins.sh b/test/scripts/jenkins_xpack_build_plugins.sh new file mode 100755 index 00000000000000..3fd3d02de13046 --- /dev/null +++ b/test/scripts/jenkins_xpack_build_plugins.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +echo " -> building kibana platform plugins" +node scripts/build_kibana_platform_plugins \ + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ + --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \ + --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \ + --workers 12 \ + --verbose diff --git a/test/scripts/jenkins_xpack_saved_objects_field_metrics.sh b/test/scripts/jenkins_xpack_saved_objects_field_metrics.sh index d3ca8839a7daba..e3b0fe778bdfb9 100755 --- a/test/scripts/jenkins_xpack_saved_objects_field_metrics.sh +++ b/test/scripts/jenkins_xpack_saved_objects_field_metrics.sh @@ -5,5 +5,5 @@ source test/scripts/jenkins_test_setup_xpack.sh checks-reporter-with-killswitch "Capture Kibana Saved Objects field count metrics" \ node scripts/functional_tests \ --debug --bail \ - --kibana-install-dir "$installDir" \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ --config test/saved_objects_field_count/config.ts; diff --git a/test/scripts/lint/eslint.sh b/test/scripts/lint/eslint.sh new file mode 100755 index 00000000000000..c3211300b96c54 --- /dev/null +++ b/test/scripts/lint/eslint.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:eslint diff --git a/test/scripts/lint/sasslint.sh b/test/scripts/lint/sasslint.sh new file mode 100755 index 00000000000000..b9c683bcb049e1 --- /dev/null +++ b/test/scripts/lint/sasslint.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:sasslint diff --git a/test/scripts/test/api_integration.sh b/test/scripts/test/api_integration.sh new file mode 100755 index 00000000000000..152c97a3ca7df7 --- /dev/null +++ b/test/scripts/test/api_integration.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:apiIntegrationTests diff --git a/test/scripts/test/jest_integration.sh b/test/scripts/test/jest_integration.sh new file mode 100755 index 00000000000000..73dbbddfb38f63 --- /dev/null +++ b/test/scripts/test/jest_integration.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_jest_integration diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh new file mode 100755 index 00000000000000..e25452698cebc8 --- /dev/null +++ b/test/scripts/test/jest_unit.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_jest diff --git a/test/scripts/test/karma_ci.sh b/test/scripts/test/karma_ci.sh new file mode 100755 index 00000000000000..e9985300ba19d4 --- /dev/null +++ b/test/scripts/test/karma_ci.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_karma_ci diff --git a/test/scripts/test/mocha.sh b/test/scripts/test/mocha.sh new file mode 100755 index 00000000000000..43c00f0a09dcf7 --- /dev/null +++ b/test/scripts/test/mocha.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:mocha diff --git a/test/scripts/test/safer_lodash_set.sh b/test/scripts/test/safer_lodash_set.sh new file mode 100755 index 00000000000000..4d7f9c28210d1a --- /dev/null +++ b/test/scripts/test/safer_lodash_set.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn run grunt run:test_package_safer_lodash_set diff --git a/test/scripts/test/xpack_jest_unit.sh b/test/scripts/test/xpack_jest_unit.sh new file mode 100755 index 00000000000000..93d70ec3553910 --- /dev/null +++ b/test/scripts/test/xpack_jest_unit.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +cd x-pack +checks-reporter-with-killswitch "X-Pack Jest" node --max-old-space-size=6144 scripts/jest --ci --verbose --maxWorkers=10 diff --git a/test/scripts/test/xpack_karma.sh b/test/scripts/test/xpack_karma.sh new file mode 100755 index 00000000000000..9078f01f1b870f --- /dev/null +++ b/test/scripts/test/xpack_karma.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +cd x-pack +checks-reporter-with-killswitch "X-Pack Karma Tests" yarn test:karma diff --git a/test/scripts/test/xpack_list_cyclic_dependency.sh b/test/scripts/test/xpack_list_cyclic_dependency.sh new file mode 100755 index 00000000000000..493fe9f58d322e --- /dev/null +++ b/test/scripts/test/xpack_list_cyclic_dependency.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +cd x-pack +checks-reporter-with-killswitch "X-Pack List cyclic dependency test" node plugins/lists/scripts/check_circular_deps diff --git a/test/scripts/test/xpack_siem_cyclic_dependency.sh b/test/scripts/test/xpack_siem_cyclic_dependency.sh new file mode 100755 index 00000000000000..b21301f25ad087 --- /dev/null +++ b/test/scripts/test/xpack_siem_cyclic_dependency.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +cd x-pack +checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node plugins/security_solution/scripts/check_circular_deps diff --git a/vars/catchErrors.groovy b/vars/catchErrors.groovy index 460a90b8ec0c04..2a1b55d832606c 100644 --- a/vars/catchErrors.groovy +++ b/vars/catchErrors.groovy @@ -1,8 +1,15 @@ // Basically, this is a shortcut for catchError(catchInterruptions: false) {} // By default, catchError will swallow aborts/timeouts, which we almost never want +// Also, by wrapping it in an additional try/catch, we cut down on spam in Pipeline Steps def call(Map params = [:], Closure closure) { - params.catchInterruptions = false - return catchError(params, closure) + try { + closure() + } catch (ex) { + params.catchInterruptions = false + catchError(params) { + throw ex + } + } } return this diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 94bfe983b4653e..5f3e8d1a0660dc 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -2,20 +2,63 @@ def withPostBuildReporting(Closure closure) { try { closure() } finally { - catchErrors { - runErrorReporter() + def parallelWorkspaces = [] + try { + parallelWorkspaces = getParallelWorkspaces() + } catch(ex) { + print ex } catchErrors { - runbld.junit() + runErrorReporter([pwd()] + parallelWorkspaces) } catchErrors { publishJunit() } + + catchErrors { + def parallelWorkspace = "${env.WORKSPACE}/parallel" + if (fileExists(parallelWorkspace)) { + dir(parallelWorkspace) { + def workspaceTasks = [:] + + parallelWorkspaces.each { workspaceDir -> + workspaceTasks[workspaceDir] = { + dir(workspaceDir) { + catchErrors { + runbld.junit() + } + } + } + } + + if (workspaceTasks) { + parallel(workspaceTasks) + } + } + } + } } } +def getParallelWorkspaces() { + def workspaces = [] + def parallelWorkspace = "${env.WORKSPACE}/parallel" + if (fileExists(parallelWorkspace)) { + dir(parallelWorkspace) { + // findFiles only returns files if you use glob, so look for a file that should be in every valid workspace + workspaces = findFiles(glob: '*/kibana/package.json') + .collect { + // get the paths to the kibana directories for the parallel workspaces + return parallelWorkspace + '/' + it.path.tokenize('/').dropRight(1).join('/') + } + } + } + + return workspaces +} + def notifyOnError(Closure closure) { try { closure() @@ -35,36 +78,43 @@ def notifyOnError(Closure closure) { } } -def functionalTestProcess(String name, Closure closure) { - return { processNumber -> - def kibanaPort = "61${processNumber}1" - def esPort = "61${processNumber}2" - def esTransportPort = "61${processNumber}3" - def ingestManagementPackageRegistryPort = "61${processNumber}4" +def withFunctionalTestEnv(List additionalEnvs = [], Closure closure) { + // This can go away once everything that uses the deprecated workers.parallelProcesses() is moved to task queue + def parallelId = env.TASK_QUEUE_PROCESS_ID ?: env.CI_PARALLEL_PROCESS_NUMBER - withEnv([ - "CI_PARALLEL_PROCESS_NUMBER=${processNumber}", - "TEST_KIBANA_HOST=localhost", - "TEST_KIBANA_PORT=${kibanaPort}", - "TEST_KIBANA_URL=http://elastic:changeme@localhost:${kibanaPort}", - "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}", - "TEST_ES_TRANSPORT_PORT=${esTransportPort}", - "INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=${ingestManagementPackageRegistryPort}", - "IS_PIPELINE_JOB=1", - "JOB=${name}", - "KBN_NP_PLUGINS_BUILT=true", - ]) { - notifyOnError { - closure() - } - } + def kibanaPort = "61${parallelId}1" + def esPort = "61${parallelId}2" + def esTransportPort = "61${parallelId}3" + def ingestManagementPackageRegistryPort = "61${parallelId}4" + + withEnv([ + "CI_GROUP=${parallelId}", + "REMOVE_KIBANA_INSTALL_DIR=1", + "CI_PARALLEL_PROCESS_NUMBER=${parallelId}", + "TEST_KIBANA_HOST=localhost", + "TEST_KIBANA_PORT=${kibanaPort}", + "TEST_KIBANA_URL=http://elastic:changeme@localhost:${kibanaPort}", + "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}", + "TEST_ES_TRANSPORT_PORT=${esTransportPort}", + "KBN_NP_PLUGINS_BUILT=true", + "INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=${ingestManagementPackageRegistryPort}", + ] + additionalEnvs) { + closure() + } +} + +def functionalTestProcess(String name, Closure closure) { + return { + withFunctionalTestEnv(["JOB=${name}"], closure) } } def functionalTestProcess(String name, String script) { return functionalTestProcess(name) { - retryable(name) { - runbld(script, "Execute ${name}") + notifyOnError { + retryable(name) { + runbld(script, "Execute ${name}") + } } } } @@ -121,11 +171,17 @@ def withGcsArtifactUpload(workerName, closure) { def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ 'target/kibana-*', + 'target/test-metrics/*', 'target/kibana-security-solution/**/*.png', 'target/junit/**/*', - 'test/**/screenshots/**/*.png', + 'target/test-suites-ci-plan.json', + 'test/**/screenshots/session/*.png', + 'test/**/screenshots/failure/*.png', + 'test/**/screenshots/diff/*.png', 'test/functional/failure_debug/html/*.html', - 'x-pack/test/**/screenshots/**/*.png', + 'x-pack/test/**/screenshots/session/*.png', + 'x-pack/test/**/screenshots/failure/*.png', + 'x-pack/test/**/screenshots/diff/*.png', 'x-pack/test/functional/failure_debug/html/*.html', 'x-pack/test/functional/apps/reporting/reports/session/*.pdf', ] @@ -140,6 +196,12 @@ def withGcsArtifactUpload(workerName, closure) { ARTIFACT_PATTERNS.each { pattern -> uploadGcsArtifact(uploadPrefix, pattern) } + + dir(env.WORKSPACE) { + ARTIFACT_PATTERNS.each { pattern -> + uploadGcsArtifact(uploadPrefix, "parallel/*/kibana/${pattern}") + } + } } } }) @@ -152,6 +214,10 @@ def withGcsArtifactUpload(workerName, closure) { def publishJunit() { junit(testResults: 'target/junit/**/*.xml', allowEmptyResults: true, keepLongStdio: true) + + dir(env.WORKSPACE) { + junit(testResults: 'parallel/*/kibana/target/junit/**/*.xml', allowEmptyResults: true, keepLongStdio: true) + } } def sendMail() { @@ -217,26 +283,36 @@ def doSetup() { } } -def buildOss() { +def buildOss(maxWorkers = '') { notifyOnError { - runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") + withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) { + runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") + } } } -def buildXpack() { +def buildXpack(maxWorkers = '') { notifyOnError { - runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") + withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) { + runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") + } } } def runErrorReporter() { + return runErrorReporter([pwd()]) +} + +def runErrorReporter(workspaces) { def status = buildUtils.getBuildStatus() def dryRun = status != "ABORTED" ? "" : "--no-github-update" + def globs = workspaces.collect { "'${it}/target/junit/**/*.xml'" }.join(" ") + bash( """ source src/dev/ci_setup/setup_env.sh - node scripts/report_failed_tests ${dryRun} target/junit/**/*.xml + node scripts/report_failed_tests ${dryRun} ${globs} """, "Report failed tests, if necessary" ) @@ -275,6 +351,102 @@ def call(Map params = [:], Closure closure) { } } +// Creates a task queue using withTaskQueue, and copies the bootstrapped kibana repo into each process's workspace +// Note that node_modules are mostly symlinked to save time/space. See test/scripts/jenkins_setup_parallel_workspace.sh +def withCiTaskQueue(Map options = [:], Closure closure) { + def setupClosure = { + // This can't use runbld, because it expects the source to be there, which isn't yet + bash("${env.WORKSPACE}/kibana/test/scripts/jenkins_setup_parallel_workspace.sh", "Set up duplicate workspace for parallel process") + } + + def config = [parallel: 24, setup: setupClosure] + options + + withTaskQueue(config) { + closure.call() + } +} + +def scriptTask(description, script) { + return { + withFunctionalTestEnv { + notifyOnError { + runbld(script, description) + } + } + } +} + +def scriptTaskDocker(description, script) { + return { + withDocker(scriptTask(description, script)) + } +} + +def buildDocker() { + sh( + script: """ + cp /usr/local/bin/runbld .ci/ + cp /usr/local/bin/bash_standard_lib.sh .ci/ + cd .ci + docker build -t kibana-ci -f ./Dockerfile . + """, + label: 'Build CI Docker image' + ) +} + +def withDocker(Closure closure) { + docker + .image('kibana-ci') + .inside( + "-v /etc/runbld:/etc/runbld:ro -v '${env.JENKINS_HOME}:${env.JENKINS_HOME}' -v '/dev/shm/workspace:/dev/shm/workspace' --shm-size 2GB --cpus 4", + closure + ) +} + +def buildOssPlugins() { + runbld('./test/scripts/jenkins_build_plugins.sh', 'Build OSS Plugins') +} + +def buildXpackPlugins() { + runbld('./test/scripts/jenkins_xpack_build_plugins.sh', 'Build X-Pack Plugins') +} + +def withTasks(Map params = [worker: [:]], Closure closure) { + catchErrors { + def config = [name: 'ci-worker', size: 'xxl', ramDisk: true] + (params.worker ?: [:]) + + workers.ci(config) { + withCiTaskQueue(parallel: 24) { + parallel([ + docker: { + retry(2) { + buildDocker() + } + }, + + // There are integration tests etc that require the plugins to be built first, so let's go ahead and build them before set up the parallel workspaces + ossPlugins: { buildOssPlugins() }, + xpackPlugins: { buildXpackPlugins() }, + ]) + + catchErrors { + closure() + } + } + } + } +} + +def allCiTasks() { + withTasks { + tasks.check() + tasks.lint() + tasks.test() + tasks.functionalOss() + tasks.functionalXpack() + } +} + def pipelineLibraryTests() { whenChanged(['vars/', '.ci/pipeline-library/']) { workers.base(size: 'flyweight', bootstrapped: false, ramDisk: false) { @@ -285,5 +457,4 @@ def pipelineLibraryTests() { } } - return this diff --git a/vars/task.groovy b/vars/task.groovy new file mode 100644 index 00000000000000..0c07b519b6fefc --- /dev/null +++ b/vars/task.groovy @@ -0,0 +1,5 @@ +def call(Closure closure) { + withTaskQueue.addTask(closure) +} + +return this diff --git a/vars/tasks.groovy b/vars/tasks.groovy new file mode 100644 index 00000000000000..52641ce31f0bed --- /dev/null +++ b/vars/tasks.groovy @@ -0,0 +1,119 @@ +def call(List closures) { + withTaskQueue.addTasks(closures) +} + +def check() { + tasks([ + kibanaPipeline.scriptTask('Check Telemetry Schema', 'test/scripts/checks/telemetry.sh'), + kibanaPipeline.scriptTask('Check TypeScript Projects', 'test/scripts/checks/ts_projects.sh'), + kibanaPipeline.scriptTask('Check Doc API Changes', 'test/scripts/checks/doc_api_changes.sh'), + kibanaPipeline.scriptTask('Check Types', 'test/scripts/checks/type_check.sh'), + kibanaPipeline.scriptTask('Check i18n', 'test/scripts/checks/i18n.sh'), + kibanaPipeline.scriptTask('Check File Casing', 'test/scripts/checks/file_casing.sh'), + kibanaPipeline.scriptTask('Check Lockfile Symlinks', 'test/scripts/checks/lock_file_symlinks.sh'), + kibanaPipeline.scriptTask('Check Licenses', 'test/scripts/checks/licenses.sh'), + kibanaPipeline.scriptTask('Verify Dependency Versions', 'test/scripts/checks/verify_dependency_versions.sh'), + kibanaPipeline.scriptTask('Verify NOTICE', 'test/scripts/checks/verify_notice.sh'), + kibanaPipeline.scriptTask('Test Projects', 'test/scripts/checks/test_projects.sh'), + kibanaPipeline.scriptTask('Test Hardening', 'test/scripts/checks/test_hardening.sh'), + ]) +} + +def lint() { + tasks([ + kibanaPipeline.scriptTask('Lint: eslint', 'test/scripts/lint/eslint.sh'), + kibanaPipeline.scriptTask('Lint: sasslint', 'test/scripts/lint/sasslint.sh'), + ]) +} + +def test() { + tasks([ + // These 2 tasks require isolation because of hard-coded, conflicting ports and such, so let's use Docker here + kibanaPipeline.scriptTaskDocker('Jest Integration Tests', 'test/scripts/test/jest_integration.sh'), + kibanaPipeline.scriptTaskDocker('Mocha Tests', 'test/scripts/test/mocha.sh'), + + kibanaPipeline.scriptTask('Jest Unit Tests', 'test/scripts/test/jest_unit.sh'), + kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh'), + kibanaPipeline.scriptTask('@elastic/safer-lodash-set Tests', 'test/scripts/test/safer_lodash_set.sh'), + kibanaPipeline.scriptTask('X-Pack SIEM cyclic dependency', 'test/scripts/test/xpack_siem_cyclic_dependency.sh'), + kibanaPipeline.scriptTask('X-Pack List cyclic dependency', 'test/scripts/test/xpack_list_cyclic_dependency.sh'), + kibanaPipeline.scriptTask('X-Pack Jest Unit Tests', 'test/scripts/test/xpack_jest_unit.sh'), + ]) +} + +def functionalOss(Map params = [:]) { + def config = params ?: [ciGroups: true, firefox: true, accessibility: true, pluginFunctional: true, visualRegression: false] + + task { + kibanaPipeline.buildOss(6) + + if (config.ciGroups) { + def ciGroups = 1..12 + tasks(ciGroups.collect { kibanaPipeline.ossCiGroupProcess(it) }) + } + + if (config.firefox) { + task(kibanaPipeline.functionalTestProcess('oss-firefox', './test/scripts/jenkins_firefox_smoke.sh')) + } + + if (config.accessibility) { + task(kibanaPipeline.functionalTestProcess('oss-accessibility', './test/scripts/jenkins_accessibility.sh')) + } + + if (config.pluginFunctional) { + task(kibanaPipeline.functionalTestProcess('oss-pluginFunctional', './test/scripts/jenkins_plugin_functional.sh')) + } + + if (config.visualRegression) { + task(kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')) + } + } +} + +def functionalXpack(Map params = [:]) { + def config = params ?: [ + ciGroups: true, + firefox: true, + accessibility: true, + pluginFunctional: true, + savedObjectsFieldMetrics:true, + pageLoadMetrics: false, + visualRegression: false, + ] + + task { + kibanaPipeline.buildXpack(10) + + if (config.ciGroups) { + def ciGroups = 1..10 + tasks(ciGroups.collect { kibanaPipeline.xpackCiGroupProcess(it) }) + } + + if (config.firefox) { + task(kibanaPipeline.functionalTestProcess('xpack-firefox', './test/scripts/jenkins_xpack_firefox_smoke.sh')) + } + + if (config.accessibility) { + task(kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh')) + } + + if (config.visualRegression) { + task(kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')) + } + + if (config.savedObjectsFieldMetrics) { + task(kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh')) + } + + whenChanged([ + 'x-pack/plugins/security_solution/', + 'x-pack/test/security_solution_cypress/', + 'x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/', + 'x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx', + ]) { + task(kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')) + } + } +} + +return this diff --git a/vars/withTaskQueue.groovy b/vars/withTaskQueue.groovy new file mode 100644 index 00000000000000..8132d6264744f2 --- /dev/null +++ b/vars/withTaskQueue.groovy @@ -0,0 +1,154 @@ +import groovy.transform.Field + +public static @Field TASK_QUEUES = [:] +public static @Field TASK_QUEUES_COUNTER = 0 + +/** + withTaskQueue creates a queue of "tasks" (just plain closures to execute), and executes them with your desired level of concurrency. + This way, you can define, for example, 40 things that need to execute, then only allow 10 of them to execute at once. + + Each "process" will execute in a separate, unique, empty directory. + If you want each process to have a bootstrapped kibana repo, check out kibanaPipeline.withCiTaskQueue + + Using the queue currently requires an agent/worker. + + Usage: + + withTaskQueue(parallel: 10) { + task { print "This is a task" } + + // This is the same as calling task() multiple times + tasks([ { print "Another task" }, { print "And another task" } ]) + + // Tasks can queue up subsequent tasks + task { + buildThing() + task { print "I depend on buildThing()" } + } + } + + You can also define a setup task that each process should execute one time before executing tasks: + withTaskQueue(parallel: 10, setup: { sh "my-setup-scrupt.sh" }) { + ... + } + +*/ +def call(Map options = [:], Closure closure) { + def config = [ parallel: 10 ] + options + def counter = ++TASK_QUEUES_COUNTER + + // We're basically abusing withEnv() to create a "scope" for all steps inside of a withTaskQueue block + // This way, we could have multiple task queue instances in the same pipeline + withEnv(["TASK_QUEUE_ID=${counter}"]) { + withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID] = [ + tasks: [], + tmpFile: sh(script: 'mktemp', returnStdout: true).trim() + ] + + closure.call() + + def processesExecuting = 0 + def processes = [:] + def iterationId = 0 + + for(def i = 1; i <= config.parallel; i++) { + def j = i + processes["task-queue-process-${j}"] = { + catchErrors { + withEnv([ + "TASK_QUEUE_PROCESS_ID=${j}", + "TASK_QUEUE_ITERATION_ID=${++iterationId}" + ]) { + dir("${WORKSPACE}/parallel/${j}/kibana") { + if (config.setup) { + config.setup.call(j) + } + + def isDone = false + while(!isDone) { // TODO some kind of timeout? + catchErrors { + if (!getTasks().isEmpty()) { + processesExecuting++ + catchErrors { + def task + try { + task = getTasks().pop() + } catch (java.util.NoSuchElementException ex) { + return + } + + task.call() + } + processesExecuting-- + // If a task finishes, and no new tasks were queued up, and nothing else is executing + // Then all of the processes should wake up and exit + if (processesExecuting < 1 && getTasks().isEmpty()) { + taskNotify() + } + return + } + + if (processesExecuting > 0) { + taskSleep() + return + } + + // Queue is empty, no processes are executing + isDone = true + } + } + } + } + } + } + } + parallel(processes) + } +} + +// If we sleep in a loop using Groovy code, Pipeline Steps is flooded with Sleep steps +// So, instead, we just watch a file and `touch` it whenever something happens that could modify the queue +// There's a 20 minute timeout just in case something goes wrong, +// in which case this method will get called again if the process is actually supposed to be waiting. +def taskSleep() { + sh(script: """#!/bin/bash + TIMESTAMP=\$(date '+%s' -d "0 seconds ago") + for (( i=1; i<=240; i++ )) + do + if [ "\$(stat -c %Y '${getTmpFile()}')" -ge "\$TIMESTAMP" ] + then + break + else + sleep 5 + if [[ \$i == 240 ]]; then + echo "Waited for new tasks for 20 minutes, exiting in case something went wrong" + fi + fi + done + """, label: "Waiting for new tasks...") +} + +// Used to let the task queue processes know that either a new task has been queued up, or work is complete +def taskNotify() { + sh "touch '${getTmpFile()}'" +} + +def getTasks() { + return withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID].tasks +} + +def getTmpFile() { + return withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID].tmpFile +} + +def addTask(Closure closure) { + getTasks() << closure + taskNotify() +} + +def addTasks(List closures) { + closures.reverse().each { + getTasks() << it + } + taskNotify() +} diff --git a/vars/workers.groovy b/vars/workers.groovy index f5a28c97c68126..e582e996a78b55 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -13,6 +13,8 @@ def label(size) { return 'docker && tests-l' case 'xl': return 'docker && tests-xl' + case 'xl-highmem': + return 'docker && tests-xl-highmem' case 'xxl': return 'docker && tests-xxl' } @@ -55,6 +57,11 @@ def base(Map params, Closure closure) { } } + sh( + script: "mkdir -p ${env.WORKSPACE}/tmp", + label: "Create custom temp directory" + ) + def checkoutInfo = [:] if (config.scm) { @@ -89,6 +96,7 @@ def base(Map params, Closure closure) { "PR_AUTHOR=${env.ghprbPullAuthorLogin ?: ''}", "TEST_BROWSER_HEADLESS=1", "GIT_BRANCH=${checkoutInfo.branch}", + "TMPDIR=${env.WORKSPACE}/tmp", // For Chrome and anything else that respects it ]) { withCredentials([ string(credentialsId: 'vault-addr', variable: 'VAULT_ADDR'), @@ -169,7 +177,9 @@ def parallelProcesses(Map params) { sleep(delay) } - processClosure(processNumber) + withEnv(["CI_PARALLEL_PROCESS_NUMBER=${processNumber}"]) { + processClosure() + } } } diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.tsx b/x-pack/plugins/canvas/storybook/storyshots.test.tsx index b51a85edaa67b8..85ec7baf18c629 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.tsx +++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import fs from 'fs'; import { ReactChildren } from 'react'; import path from 'path'; import moment from 'moment'; @@ -94,6 +95,12 @@ jest.mock('../shareable_runtime/components/rendered_element'); // @ts-expect-error RenderedElement.mockImplementation(() => 'RenderedElement'); +// Some of the code requires that this directory exists, but the tests don't actually require any css to be present +const cssDir = path.resolve(__dirname, '../../../../built_assets/css'); +if (!fs.existsSync(cssDir)) { + fs.mkdirSync(cssDir, { recursive: true }); +} + addSerializer(styleSheetSerializer); // Initialize Storyshots and build the Jest Snapshots From 0502fee6518b81e39afca0f28df2a39b8397c36a Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 4 Aug 2020 10:19:51 -0700 Subject: [PATCH 57/74] [kbn/optimizer] remove unused modules (#74195) Co-authored-by: spalger --- packages/kbn-optimizer/package.json | 3 --- yarn.lock | 9 ++------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index 4fbbc920c4447a..e6eb5de31abd87 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -15,12 +15,9 @@ "@kbn/dev-utils": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", "@types/compression-webpack-plugin": "^2.0.2", - "@types/estree": "^0.0.44", "@types/loader-utils": "^1.1.3", "@types/watchpack": "^1.1.5", "@types/webpack": "^4.41.3", - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1", "autoprefixer": "^9.7.4", "babel-loader": "^8.0.6", "clean-webpack-plugin": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index a70f04e030447c..0638267afd2566 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4527,11 +4527,6 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/estree@^0.0.44": - version "0.0.44" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.44.tgz#980cc5a29a3ef3bea6ff1f7d021047d7ea575e21" - integrity sha512-iaIVzr+w2ZJ5HkidlZ3EJM8VTZb2MJLCjw3V+505yVts0gRC4UMvjw0d1HPtGqI/HQC/KdsYtayfzl+AXY2R8g== - "@types/events@*": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" @@ -6188,7 +6183,7 @@ acorn-walk@^6.0.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913" integrity sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw== -acorn-walk@^7.0.0, acorn-walk@^7.1.1: +acorn-walk@^7.0.0: version "7.1.1" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e" integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ== @@ -6218,7 +6213,7 @@ acorn@^7.0.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.2.0.tgz#17ea7e40d7c8640ff54a694c889c26f31704effe" integrity sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ== -acorn@^7.1.0, acorn@^7.1.1: +acorn@^7.1.0: version "7.1.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== From 3067184c45caf67c64096fa2a7fbde21a97738fd Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 4 Aug 2020 13:30:09 -0400 Subject: [PATCH 58/74] [Maps] Custom color ramps should show correctly on the map for mvt layers (#74169) --- .../public/classes/fields/es_agg_field.ts | 4 +++ .../maps/public/classes/fields/field.ts | 9 +++++ .../maps/public/classes/fields/mvt_field.ts | 4 +++ .../fields/top_term_percentage_field.ts | 4 +++ .../properties/dynamic_color_property.js | 10 ++---- .../properties/dynamic_color_property.test.js | 36 ++++++++++++++----- .../properties/dynamic_size_property.js | 8 ++--- .../properties/dynamic_style_property.tsx | 16 ++++++--- 8 files changed, 65 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts index 15779d22681c0f..7b184819b839b4 100644 --- a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts @@ -134,6 +134,10 @@ export class ESAggField implements IESAggField { supportsAutoDomain(): boolean { return true; } + + canReadFromGeoJson(): boolean { + return true; + } } export function esAggFieldsFactory( diff --git a/x-pack/plugins/maps/public/classes/fields/field.ts b/x-pack/plugins/maps/public/classes/fields/field.ts index 410b38e79ffe4e..2c190d54f02651 100644 --- a/x-pack/plugins/maps/public/classes/fields/field.ts +++ b/x-pack/plugins/maps/public/classes/fields/field.ts @@ -26,7 +26,12 @@ export interface IField { // then styling properties that require the domain to be known cannot use this property. supportsAutoDomain(): boolean; + // Determinse wheter Maps-app can automatically deterime the domain of the field-values + // _without_ having to retrieve the data as GeoJson + // e.g. for ES-sources, this would use the extended_stats API supportsFieldMeta(): boolean; + + canReadFromGeoJson(): boolean; } export class AbstractField implements IField { @@ -90,4 +95,8 @@ export class AbstractField implements IField { supportsAutoDomain(): boolean { return true; } + + canReadFromGeoJson(): boolean { + return true; + } } diff --git a/x-pack/plugins/maps/public/classes/fields/mvt_field.ts b/x-pack/plugins/maps/public/classes/fields/mvt_field.ts index eb2bb94b36a690..7c8d08bacdb512 100644 --- a/x-pack/plugins/maps/public/classes/fields/mvt_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/mvt_field.ts @@ -56,4 +56,8 @@ export class MVTField extends AbstractField implements IField { supportsAutoDomain() { return false; } + + canReadFromGeoJson(): boolean { + return false; + } } diff --git a/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts b/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts index f4625e42ab5deb..fc931b13619ef3 100644 --- a/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts @@ -79,4 +79,8 @@ export class TopTermPercentageField implements IESAggField { canValueBeFormatted(): boolean { return false; } + + canReadFromGeoJson(): boolean { + return true; + } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js index cde4d1f2011990..e643abcaf8d548 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js @@ -8,7 +8,7 @@ import { DynamicStyleProperty } from './dynamic_style_property'; import { makeMbClampedNumberExpression, dynamicRound } from '../style_util'; import { getOrdinalMbColorRampStops, getColorPalette } from '../../color_palettes'; import React from 'react'; -import { COLOR_MAP_TYPE, MB_LOOKUP_FUNCTION } from '../../../../../common/constants'; +import { COLOR_MAP_TYPE } from '../../../../../common/constants'; import { isCategoricalStopsInvalid, getOtherCategoryLabel, @@ -91,10 +91,6 @@ export class DynamicColorProperty extends DynamicStyleProperty { return colors ? colors.length : 0; } - supportsMbFeatureState() { - return true; - } - _getMbColor() { if (!this._field || !this._field.getName()) { return null; @@ -120,7 +116,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { const lessThanFirstStopValue = firstStopValue - 1; return [ 'step', - ['coalesce', ['feature-state', targetName], lessThanFirstStopValue], + ['coalesce', [this.getMbLookupFunction(), targetName], lessThanFirstStopValue], RGBA_0000, // MB will assign the base value to any features that is below the first stop value ...colorStops, ]; @@ -146,7 +142,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { makeMbClampedNumberExpression({ minValue: rangeFieldMeta.min, maxValue: rangeFieldMeta.max, - lookupFunction: MB_LOOKUP_FUNCTION.FEATURE_STATE, + lookupFunction: this.getMbLookupFunction(), fallback: lessThanFirstStopValue, fieldName: targetName, }), diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js index 2183a298a28417..425954c9af860a 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js @@ -343,6 +343,15 @@ describe('get mapbox color expression (via internal _getMbColor)', () => { }); describe('custom color ramp', () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.ORDINAL, + useCustomColorRamp: true, + customColorRamp: [ + { stop: 10, color: '#f7faff' }, + { stop: 100, color: '#072f6b' }, + ], + }; + test('should return null when customColorRamp is not provided', async () => { const dynamicStyleOptions = { type: COLOR_MAP_TYPE.ORDINAL, @@ -362,15 +371,7 @@ describe('get mapbox color expression (via internal _getMbColor)', () => { expect(colorProperty._getMbColor()).toBeNull(); }); - test('should return mapbox expression for custom color ramp', async () => { - const dynamicStyleOptions = { - type: COLOR_MAP_TYPE.ORDINAL, - useCustomColorRamp: true, - customColorRamp: [ - { stop: 10, color: '#f7faff' }, - { stop: 100, color: '#072f6b' }, - ], - }; + test('should use `feature-state` by default', async () => { const colorProperty = makeProperty(dynamicStyleOptions); expect(colorProperty._getMbColor()).toEqual([ 'step', @@ -382,6 +383,23 @@ describe('get mapbox color expression (via internal _getMbColor)', () => { '#072f6b', ]); }); + + test('should use `get` when source cannot return raw geojson', async () => { + const field = Object.create(mockField); + field.canReadFromGeoJson = function () { + return false; + }; + const colorProperty = makeProperty(dynamicStyleOptions, undefined, field); + expect(colorProperty._getMbColor()).toEqual([ + 'step', + ['coalesce', ['get', 'foobar'], 9], + 'rgba(0,0,0,0)', + 10, + '#f7faff', + 100, + '#072f6b', + ]); + }); }); }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js index 662d1ccf33b95d..83bd4b70ba5c33 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js @@ -33,7 +33,7 @@ export class DynamicSizeProperty extends DynamicStyleProperty { return false; } - return true; + return super.supportsMbFeatureState(); } syncHaloWidthWithMb(mbLayerId, mbMap) { @@ -109,17 +109,13 @@ export class DynamicSizeProperty extends DynamicStyleProperty { } _getMbDataDrivenSize({ targetName, minSize, maxSize, minValue, maxValue }) { - const lookup = this.supportsMbFeatureState() - ? MB_LOOKUP_FUNCTION.FEATURE_STATE - : MB_LOOKUP_FUNCTION.GET; - const stops = minValue === maxValue ? [maxValue, maxSize] : [minValue, minSize, maxValue, maxSize]; return [ 'interpolate', ['linear'], makeMbClampedNumberExpression({ - lookupFunction: lookup, + lookupFunction: this.getMbLookupFunction(), maxValue, minValue, fieldName: targetName, diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx index f666a9aad78048..18b7faf6283c46 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx @@ -11,9 +11,10 @@ import { Feature } from 'geojson'; import { AbstractStyleProperty, IStyleProperty } from './style_property'; import { DEFAULT_SIGMA } from '../vector_style_defaults'; import { - STYLE_TYPE, - SOURCE_META_DATA_REQUEST_ID, FIELD_ORIGIN, + MB_LOOKUP_FUNCTION, + SOURCE_META_DATA_REQUEST_ID, + STYLE_TYPE, VECTOR_STYLES, } from '../../../../../common/constants'; import { OrdinalFieldMetaPopover } from '../components/field_meta/ordinal_field_meta_popover'; @@ -21,8 +22,8 @@ import { CategoricalFieldMetaPopover } from '../components/field_meta/categorica import { CategoryFieldMeta, FieldMetaOptions, - StyleMetaData, RangeFieldMeta, + StyleMetaData, } from '../../../../../common/descriptor_types'; import { IField } from '../../../fields/field'; import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; @@ -41,6 +42,7 @@ export interface IDynamicStyleProperty extends IStyleProperty { supportsFieldMeta(): boolean; getFieldMetaRequest(): Promise; supportsMbFeatureState(): boolean; + getMbLookupFunction(): MB_LOOKUP_FUNCTION; pluckOrdinalStyleMetaFromFeatures(features: Feature[]): RangeFieldMeta | null; pluckCategoricalStyleMetaFromFeatures(features: Feature[]): CategoryFieldMeta | null; getValueSuggestions(query: string): Promise; @@ -193,7 +195,13 @@ export class DynamicStyleProperty extends AbstractStyleProperty } supportsMbFeatureState() { - return true; + return !!this._field && this._field.canReadFromGeoJson(); + } + + getMbLookupFunction(): MB_LOOKUP_FUNCTION { + return this.supportsMbFeatureState() + ? MB_LOOKUP_FUNCTION.FEATURE_STATE + : MB_LOOKUP_FUNCTION.GET; } getFieldMetaOptions() { From de22caa0f657812378c0bfbdec728f2b4a0d852a Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Tue, 4 Aug 2020 13:32:21 -0400 Subject: [PATCH 59/74] [Monitoring] Ensure setup mode works on cloud but only for alerts (#73127) * Ensure setup mode works on cloud but only for alerts * Update snapshot * Update translations * Restructure tests to understand the failure better * PR feedback * Backwards, whoops * Remove commented out code Co-authored-by: Elastic Machine --- x-pack/plugins/monitoring/common/enums.ts | 5 ++ .../monitoring/public/alerts/badge.tsx | 2 +- .../components/apm/instances/instances.js | 6 ++- .../components/beats/listing/listing.js | 6 ++- .../components/cluster/overview/apm_panel.js | 21 ++++---- .../cluster/overview/beats_panel.js | 21 ++++---- .../cluster/overview/elasticsearch_panel.js | 21 ++++---- .../cluster/overview/kibana_panel.js | 21 ++++---- .../cluster/overview/logstash_panel.js | 21 ++++---- .../components/elasticsearch/nodes/nodes.js | 12 +++-- .../components/kibana/instances/instances.js | 6 ++- .../components/logstash/listing/listing.js | 6 ++- .../__snapshots__/setup_mode.test.js.snap | 2 + .../public/components/renderers/setup_mode.js | 6 ++- .../__snapshots__/enter_button.test.tsx.snap | 1 + .../__snapshots__/tooltip.test.js.snap | 25 +++++++++ .../components/setup_mode/enter_button.tsx | 2 +- .../public/components/setup_mode/tooltip.js | 6 ++- .../public/components/table/eui_table.js | 4 +- .../public/components/table/eui_table_ssp.js | 8 ++- .../public/directives/main/index.js | 9 +++- .../monitoring/public/lib/setup_mode.tsx | 21 +++++--- .../public/views/base_controller.js | 6 +-- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../apps/monitoring/_get_lifecycle_methods.js | 6 +-- .../test/functional/apps/monitoring/index.js | 2 + .../monitoring/setup/metricbeat_migration.js | 51 +++++++++++++++++++ x-pack/test/functional/services/index.ts | 2 + .../functional/services/monitoring/index.js | 1 + .../services/monitoring/setup_mode.js | 37 ++++++++++++++ 31 files changed, 260 insertions(+), 79 deletions(-) create mode 100644 x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js create mode 100644 x-pack/test/functional/services/monitoring/setup_mode.js diff --git a/x-pack/plugins/monitoring/common/enums.ts b/x-pack/plugins/monitoring/common/enums.ts index 74711b31756beb..d4058e9de801ed 100644 --- a/x-pack/plugins/monitoring/common/enums.ts +++ b/x-pack/plugins/monitoring/common/enums.ts @@ -26,3 +26,8 @@ export enum AlertParamType { Duration = 'duration', Percentage = 'percentage', } + +export enum SetupModeFeature { + MetricbeatMigration = 'metricbeatMigration', + Alerts = 'alerts', +} diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx index 02963e9457ab5b..1d67eebb1705cc 100644 --- a/x-pack/plugins/monitoring/public/alerts/badge.tsx +++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx @@ -180,7 +180,7 @@ export const AlertsBadge: React.FC = (props: Props) => { } return ( - + {badges.map((badge, index) => ( {badge} diff --git a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js index 7754af1be85881..6dcfa6dd043aaf 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js +++ b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js @@ -26,6 +26,8 @@ import { APM_SYSTEM_ID } from '../../../../common/constants'; import { ListingCallOut } from '../../setup_mode/listing_callout'; import { SetupModeBadge } from '../../setup_mode/badge'; import { FormattedMessage } from '@kbn/i18n/react'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; function getColumns(setupMode) { return [ @@ -36,7 +38,7 @@ function getColumns(setupMode) { field: 'name', render: (name, apm) => { let setupModeStatus = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { const list = get(setupMode, 'data.byUuid', {}); const status = list[apm.uuid] || {}; const instance = { @@ -129,7 +131,7 @@ export function ApmServerInstances({ apms, setupMode }) { const { pagination, sorting, onTableChange, data } = apms; let setupModeCallout = null; - if (setupMode.enabled && setupMode.data) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { setupModeCallout = ( { let setupModeStatus = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { const list = get(setupMode, 'data.byUuid', {}); const status = list[beat.uuid] || {}; const instance = { @@ -122,7 +124,7 @@ export class Listing extends PureComponent { const { stats, data, sorting, pagination, onTableChange, setupMode } = this.props; let setupModeCallOut = null; - if (setupMode.enabled && setupMode.data) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { setupModeCallOut = ( getSafeForExternalLink('#/apm/instances'); const setupModeData = get(setupMode.data, 'apm'); - const setupModeTooltip = - setupMode && setupMode.enabled ? ( - - ) : null; + const setupModeMetricbeatMigrationTooltip = isSetupModeFeatureEnabled( + SetupModeFeature.MetricbeatMigration + ) ? ( + + ) : null; return ( - {setupModeTooltip} + {setupModeMetricbeatMigrationTooltip} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js index df0070176727ea..3591ad178f4cd4 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js @@ -25,6 +25,8 @@ import { i18n } from '@kbn/i18n'; import { SetupModeTooltip } from '../../setup_mode/tooltip'; import { BEATS_SYSTEM_ID } from '../../../../common/constants'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; export function BeatsPanel(props) { const { setupMode } = props; @@ -35,14 +37,15 @@ export function BeatsPanel(props) { } const setupModeData = get(setupMode.data, 'beats'); - const setupModeTooltip = - setupMode && setupMode.enabled ? ( - - ) : null; + const setupModeMetricbeatMigrationTooltip = isSetupModeFeatureEnabled( + SetupModeFeature.MetricbeatMigration + ) ? ( + + ) : null; const beatTypes = props.beats.types.map((beat, index) => { return [ @@ -142,7 +145,7 @@ export function BeatsPanel(props) { - {setupModeTooltip} + {setupModeMetricbeatMigrationTooltip} {beatTypes} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index edf4c5d73f837f..34e995510cf72c 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -44,6 +44,8 @@ import { } from '../../../../common/constants'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; +import { SetupModeFeature } from '../../../../common/enums'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; const calculateShards = (shards) => { const total = get(shards, 'total', 0); @@ -172,14 +174,15 @@ export function ElasticsearchPanel(props) { const { primaries, replicas } = calculateShards(get(props, 'cluster_stats.indices.shards', {})); const setupModeData = get(setupMode.data, 'elasticsearch'); - const setupModeTooltip = - setupMode && setupMode.enabled ? ( - - ) : null; + const setupModeMetricbeatMigrationTooltip = isSetupModeFeatureEnabled( + SetupModeFeature.MetricbeatMigration + ) ? ( + + ) : null; const showMlJobs = () => { // if license doesn't support ML, then `ml === null` @@ -367,7 +370,7 @@ export function ElasticsearchPanel(props) { - {setupModeTooltip} + {setupModeMetricbeatMigrationTooltip} {nodesAlertStatus} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js index eb1f82eb5550dc..6fa533302db48d 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js @@ -32,6 +32,8 @@ import { KIBANA_SYSTEM_ID, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../com import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; const INSTANCES_PANEL_ALERTS = [ALERT_KIBANA_VERSION_MISMATCH]; @@ -50,14 +52,15 @@ export function KibanaPanel(props) { const goToInstances = () => getSafeForExternalLink('#/kibana/instances'); const setupModeData = get(setupMode.data, 'kibana'); - const setupModeTooltip = - setupMode && setupMode.enabled ? ( - - ) : null; + const setupModeMetricbeatMigrationTooltip = isSetupModeFeatureEnabled( + SetupModeFeature.MetricbeatMigration + ) ? ( + + ) : null; let instancesAlertStatus = null; if (shouldShowAlertBadge(alerts, INSTANCES_PANEL_ALERTS)) { @@ -165,7 +168,7 @@ export function KibanaPanel(props) { - {setupModeTooltip} + {setupModeMetricbeatMigrationTooltip} {instancesAlertStatus} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js index 7c9758bc0ddb60..9b4a50271a2470 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js @@ -37,6 +37,8 @@ import { SetupModeTooltip } from '../../setup_mode/tooltip'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; const NODES_PANEL_ALERTS = [ALERT_LOGSTASH_VERSION_MISMATCH]; @@ -56,14 +58,15 @@ export function LogstashPanel(props) { const goToPipelines = () => getSafeForExternalLink('#/logstash/pipelines'); const setupModeData = get(setupMode.data, 'logstash'); - const setupModeTooltip = - setupMode && setupMode.enabled ? ( - - ) : null; + const setupModeMetricbeatMigrationTooltip = isSetupModeFeatureEnabled( + SetupModeFeature.MetricbeatMigration + ) ? ( + + ) : null; let nodesAlertStatus = null; if (shouldShowAlertBadge(alerts, NODES_PANEL_ALERTS)) { @@ -162,7 +165,7 @@ export function LogstashPanel(props) { - {setupModeTooltip} + {setupModeMetricbeatMigrationTooltip} {nodesAlertStatus} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index b7463fe6532b7b..43512f8e528f6a 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -32,6 +32,8 @@ import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; import { ListingCallOut } from '../../setup_mode/listing_callout'; import { AlertsStatus } from '../../../alerts/status'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; const getNodeTooltip = (node) => { const { nodeTypeLabel, nodeTypeClass } = node; @@ -85,7 +87,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler ); let setupModeStatus = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { const list = _.get(setupMode, 'data.byUuid', {}); const status = list[node.resolver] || {}; const instance = { @@ -309,7 +311,11 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear // Merge the nodes data with the setup data if enabled const nodes = props.nodes || []; - if (setupMode.enabled && setupMode.data) { + if ( + setupMode && + setupMode.enabled && + isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration) + ) { // We want to create a seamless experience for the user by merging in the setup data // and the node data from monitoring indices in the likely scenario where some nodes // are using MB collection and some are using no collection @@ -332,7 +338,7 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear } let setupModeCallout = null; - if (setupMode.enabled && setupMode.data) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { setupModeCallout = ( { const columns = [ @@ -39,7 +41,7 @@ const getColumns = (setupMode, alerts) => { field: 'name', render: (name, kibana) => { let setupModeStatus = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { const list = get(setupMode, 'data.byUuid', {}); const uuid = get(kibana, 'kibana.uuid'); const status = list[uuid] || {}; @@ -166,7 +168,7 @@ export class KibanaInstances extends PureComponent { let setupModeCallOut = null; // Merge the instances data with the setup data if enabled const instances = this.props.instances || []; - if (setupMode.enabled && setupMode.data) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { // We want to create a seamless experience for the user by merging in the setup data // and the node data from monitoring indices in the likely scenario where some instances // are using MB collection and some are using no collection diff --git a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js index caa21e5e692920..4a1137079ebb41 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js @@ -25,6 +25,8 @@ import { SetupModeBadge } from '../../setup_mode/badge'; import { ListingCallOut } from '../../setup_mode/listing_callout'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { AlertsStatus } from '../../../alerts/status'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; +import { SetupModeFeature } from '../../../../common/enums'; export class Listing extends PureComponent { getColumns() { @@ -40,7 +42,7 @@ export class Listing extends PureComponent { sortable: true, render: (name, node) => { let setupModeStatus = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { const list = get(setupMode, 'data.byUuid', {}); const uuid = get(node, 'logstash.uuid'); const status = list[uuid] || {}; @@ -167,7 +169,7 @@ export class Listing extends PureComponent { })); let setupModeCallOut = null; - if (setupMode.enabled && setupMode.data) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { setupModeCallOut = ( - + diff --git a/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap index 2eaa25803c81ec..0d9e50d14657b9 100644 --- a/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap +++ b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap @@ -3,6 +3,7 @@ exports[`EnterButton should render properly 1`] = `
= ( } return ( -
+
{tooltip}; + return ( + + {tooltip} + + ); } diff --git a/x-pack/plugins/monitoring/public/components/table/eui_table.js b/x-pack/plugins/monitoring/public/components/table/eui_table.js index cc58d7267f6dc7..44ee883c135d22 100644 --- a/x-pack/plugins/monitoring/public/components/table/eui_table.js +++ b/x-pack/plugins/monitoring/public/components/table/eui_table.js @@ -8,6 +8,8 @@ import React, { Fragment } from 'react'; import { EuiInMemoryTable, EuiButton, EuiSpacer, EuiSearchBar } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getIdentifier } from '../setup_mode/formatting'; +import { isSetupModeFeatureEnabled } from '../../lib/setup_mode'; +import { SetupModeFeature } from '../../../common/enums'; export function EuiMonitoringTable({ rows: items, @@ -45,7 +47,7 @@ export function EuiMonitoringTable({ }); let footerContent = null; - if (setupMode && setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { footerContent = ( diff --git a/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js b/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js index 618547398bd96a..9b4b086a0208b2 100644 --- a/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js +++ b/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js @@ -8,6 +8,8 @@ import React, { Fragment } from 'react'; import { EuiBasicTable, EuiSpacer, EuiSearchBar, EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getIdentifier } from '../setup_mode/formatting'; +import { isSetupModeFeatureEnabled } from '../../lib/setup_mode'; +import { SetupModeFeature } from '../../../common/enums'; export function EuiMonitoringSSPTable({ rows: items, @@ -46,7 +48,11 @@ export function EuiMonitoringSSPTable({ }); let footerContent = null; - if (setupMode && setupMode.enabled) { + if ( + setupMode && + setupMode.enabled && + isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration) + ) { footerContent = ( diff --git a/x-pack/plugins/monitoring/public/directives/main/index.js b/x-pack/plugins/monitoring/public/directives/main/index.js index eda32cd39c0d0b..d682e87b7ca95f 100644 --- a/x-pack/plugins/monitoring/public/directives/main/index.js +++ b/x-pack/plugins/monitoring/public/directives/main/index.js @@ -11,9 +11,14 @@ import { get } from 'lodash'; import template from './index.html'; import { Legacy } from '../../legacy_shims'; import { shortenPipelineHash } from '../../../common/formatting'; -import { getSetupModeState, initSetupModeState } from '../../lib/setup_mode'; +import { + getSetupModeState, + initSetupModeState, + isSetupModeFeatureEnabled, +} from '../../lib/setup_mode'; import { Subscription } from 'rxjs'; import { getSafeForExternalLink } from '../../lib/get_safe_for_external_link'; +import { SetupModeFeature } from '../../../common/enums'; const setOptions = (controller) => { if ( @@ -179,7 +184,7 @@ export class MonitoringMainController { isDisabledTab(product) { const setupMode = getSetupModeState(); - if (!setupMode.enabled || !setupMode.data) { + if (!isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { return false; } diff --git a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx index a36b945e82ef78..b99093a3d8ad19 100644 --- a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { Legacy } from '../legacy_shims'; import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; +import { SetupModeFeature } from '../../common/enums'; function isOnPage(hash: string) { return includes(window.location.hash, hash); @@ -93,16 +94,12 @@ export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid); setupModeState.data = data; const hasPermissions = get(data, '_meta.hasPermissions', false); - if (Legacy.shims.isCloud || !hasPermissions) { + if (!hasPermissions) { let text: string = ''; if (!hasPermissions) { text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { defaultMessage: 'You do not have the necessary permissions to do this.', }); - } else { - text = i18n.translate('xpack.monitoring.setupMode.notAvailableCloud', { - defaultMessage: 'This feature is not available on cloud.', - }); } angularState.scope.$evalAsync(() => { @@ -180,7 +177,7 @@ export const setSetupModeMenuItem = () => { } const globalState = angularState.injector.get('globalState'); - const enabled = !globalState.inSetupMode && !Legacy.shims.isCloud; + const enabled = !globalState.inSetupMode; render( , @@ -212,3 +209,15 @@ export const isInSetupMode = () => { const globalState = $injector.get('globalState'); return globalState.inSetupMode; }; + +export const isSetupModeFeatureEnabled = (feature: SetupModeFeature) => { + if (!setupModeState.enabled) { + return false; + } + if (feature === SetupModeFeature.MetricbeatMigration) { + if (Legacy.shims.isCloud) { + return false; + } + } + return true; +}; diff --git a/x-pack/plugins/monitoring/public/views/base_controller.js b/x-pack/plugins/monitoring/public/views/base_controller.js index 2f88245d88c4a5..a41d4ec4bbfa28 100644 --- a/x-pack/plugins/monitoring/public/views/base_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_controller.js @@ -11,7 +11,8 @@ import { getPageData } from '../lib/get_page_data'; import { PageLoading } from '../components'; import { Legacy } from '../legacy_shims'; import { PromiseWithCancel } from '../../common/cancel_promise'; -import { updateSetupModeData, getSetupModeState } from '../lib/setup_mode'; +import { SetupModeFeature } from '../../common/enums'; +import { updateSetupModeData, isSetupModeFeatureEnabled } from '../lib/setup_mode'; /** * Given a timezone, this function will calculate the offset in milliseconds @@ -150,11 +151,10 @@ export class MonitoringViewBaseController { } const _api = apiUrlFn ? apiUrlFn() : api; const promises = [_getPageData($injector, _api, this.getPaginationRouteOptions())]; - const setupMode = getSetupModeState(); if (alerts.shouldFetch) { promises.push(fetchAlerts()); } - if (setupMode.enabled) { + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { promises.push(updateSetupModeData()); } this.updateDataPromise = new PromiseWithCancel(Promise.all(promises)); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 544f6cc6ec342a..aa12e2a34075ea 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -14033,7 +14033,6 @@ "xpack.monitoring.setupMode.node": "ノード", "xpack.monitoring.setupMode.nodes": "ノード", "xpack.monitoring.setupMode.noMonitoringDataFound": "{product} {identifier} が検出されませんでした", - "xpack.monitoring.setupMode.notAvailableCloud": "この機能はクラウドで使用できません。", "xpack.monitoring.setupMode.notAvailablePermissions": "これを実行するために必要な権限がありません。", "xpack.monitoring.setupMode.notAvailableTitle": "設定モードは使用できません", "xpack.monitoring.setupMode.server": "サーバー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2ee85fc87cc728..befce4de3d2471 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -14038,7 +14038,6 @@ "xpack.monitoring.setupMode.node": "节点", "xpack.monitoring.setupMode.nodes": "节点", "xpack.monitoring.setupMode.noMonitoringDataFound": "未检测到 {product} {identifier}", - "xpack.monitoring.setupMode.notAvailableCloud": "此功能在云上不可用。", "xpack.monitoring.setupMode.notAvailablePermissions": "您没有所需的权限来执行此功能。", "xpack.monitoring.setupMode.notAvailableTitle": "设置模式不可用", "xpack.monitoring.setupMode.server": "服务器", diff --git a/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js b/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js index d01883e9ea5497..3564c72cfc3484 100644 --- a/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js +++ b/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js @@ -8,11 +8,10 @@ export const getLifecycleMethods = (getService, getPageObjects) => { const esArchiver = getService('esArchiver'); const security = getService('security'); const PageObjects = getPageObjects(['monitoring', 'timePicker', 'security']); - const noData = getService('monitoringNoData'); let _archive; return { - async setup(archive, { from, to }) { + async setup(archive, { from, to, useSuperUser = false }) { _archive = archive; const kibanaServer = getService('kibanaServer'); @@ -24,8 +23,7 @@ export const getLifecycleMethods = (getService, getPageObjects) => { await esArchiver.load(archive); await kibanaServer.uiSettings.replace({}); - await PageObjects.monitoring.navigateTo(); - await noData.isOnNoDataPage(); + await PageObjects.monitoring.navigateTo(useSuperUser); // pause autorefresh in the time filter because we don't wait any ticks, // and we don't want ES to log a warning when data gets wiped out diff --git a/x-pack/test/functional/apps/monitoring/index.js b/x-pack/test/functional/apps/monitoring/index.js index c383d8593a4fac..6a2037bbc49249 100644 --- a/x-pack/test/functional/apps/monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/index.js @@ -39,5 +39,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./time_filter')); loadTestFile(require.resolve('./enable_monitoring')); + + loadTestFile(require.resolve('./setup/metricbeat_migration')); }); } diff --git a/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js b/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js new file mode 100644 index 00000000000000..34ac06450f6843 --- /dev/null +++ b/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { getLifecycleMethods } from '../_get_lifecycle_methods'; + +export default function ({ getService, getPageObjects }) { + const setupMode = getService('monitoringSetupMode'); + const PageObjects = getPageObjects(['common', 'console']); + + describe('Setup mode metricbeat migration', function () { + describe('setup mode btn', () => { + const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); + + before(async () => { + await setup('monitoring/setup/collection/es_and_kibana_mb', { + from: 'Apr 9, 2019 @ 00:00:00.741', + to: 'Apr 9, 2019 @ 23:59:59.741', + useSuperUser: true, + }); + }); + + after(async () => { + await tearDown(); + }); + + it('should exist', async () => { + expect(await setupMode.doesSetupModeBtnAppear()).to.be(true); + }); + + it('should be clickable and show the bottom bar', async () => { + await setupMode.clickSetupModeBtn(); + await PageObjects.common.sleep(1000); // bottom drawer animation + expect(await setupMode.doesBottomBarAppear()).to.be(true); + }); + + it('should not show metricbeat migration if cloud', async () => { + const isCloud = await PageObjects.common.isCloud(); + expect(await setupMode.doesMetricbeatMigrationTooltipAppear()).to.be(!isCloud); + }); + + // TODO: this does not work because TLS isn't enabled in the test env + // it('should show alerts all the time', async () => { + // expect(await setupMode.doesAlertsTooltipAppear()).to.be(true); + // }); + }); + }); +} diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 4b6342758be93a..6d8eade25d7e6d 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -30,6 +30,7 @@ import { MonitoringKibanaInstancesProvider, MonitoringKibanaInstanceProvider, MonitoringKibanaSummaryStatusProvider, + MonitoringSetupModeProvider, // @ts-ignore not ts yet } from './monitoring'; // @ts-ignore not ts yet @@ -85,6 +86,7 @@ export const services = { monitoringKibanaInstances: MonitoringKibanaInstancesProvider, monitoringKibanaInstance: MonitoringKibanaInstanceProvider, monitoringKibanaSummaryStatus: MonitoringKibanaSummaryStatusProvider, + monitoringSetupMode: MonitoringSetupModeProvider, pipelineList: PipelineListProvider, pipelineEditor: PipelineEditorProvider, random: RandomProvider, diff --git a/x-pack/test/functional/services/monitoring/index.js b/x-pack/test/functional/services/monitoring/index.js index 0087cbaae2b08f..0187a3f9768372 100644 --- a/x-pack/test/functional/services/monitoring/index.js +++ b/x-pack/test/functional/services/monitoring/index.js @@ -25,3 +25,4 @@ export { MonitoringKibanaOverviewProvider } from './kibana_overview'; export { MonitoringKibanaInstancesProvider } from './kibana_instances'; export { MonitoringKibanaInstanceProvider } from './kibana_instance'; export { MonitoringKibanaSummaryStatusProvider } from './kibana_summary_status'; +export { MonitoringSetupModeProvider } from './setup_mode'; diff --git a/x-pack/test/functional/services/monitoring/setup_mode.js b/x-pack/test/functional/services/monitoring/setup_mode.js new file mode 100644 index 00000000000000..a71ad924a852ff --- /dev/null +++ b/x-pack/test/functional/services/monitoring/setup_mode.js @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function MonitoringSetupModeProvider({ getService }) { + const testSubjects = getService('testSubjects'); + + const SUBJ_SETUP_MODE_BTN = 'monitoringSetupModeBtn'; + const SUBJ_SETUP_MODE_BOTTOM_BAR = 'monitoringSetupModeBottomBar'; + const SUBJ_SETUP_MODE_METRICBEAT_MIGRATION_TOOLTIP = + 'monitoringSetupModeMetricbeatMigrationTooltip'; + const SUBJ_SETUP_MODE_ALERTS_BADGE = 'monitoringSetupModeAlertBadges'; + + return new (class SetupMode { + async doesSetupModeBtnAppear() { + return await testSubjects.exists(SUBJ_SETUP_MODE_BTN); + } + + async clickSetupModeBtn() { + return await testSubjects.click(SUBJ_SETUP_MODE_BTN); + } + + async doesBottomBarAppear() { + return await testSubjects.exists(SUBJ_SETUP_MODE_BOTTOM_BAR); + } + + async doesMetricbeatMigrationTooltipAppear() { + return await testSubjects.exists(SUBJ_SETUP_MODE_METRICBEAT_MIGRATION_TOOLTIP); + } + + async doesAlertsTooltipAppear() { + return await testSubjects.exists(SUBJ_SETUP_MODE_ALERTS_BADGE); + } + })(); +} From b7a8a4a54ada7cd16d03d23064c9d378cb292c6a Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 4 Aug 2020 14:19:26 -0400 Subject: [PATCH 60/74] [CI] Fix baseline_capture job by adding parallel process number back --- .ci/Jenkinsfile_baseline_capture | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture index b0d3591821642a..9a49c19b94df22 100644 --- a/.ci/Jenkinsfile_baseline_capture +++ b/.ci/Jenkinsfile_baseline_capture @@ -7,18 +7,22 @@ kibanaPipeline(timeoutMinutes: 120) { githubCommitStatus.trackBuild(params.commit, 'kibana-ci-baseline') { ciStats.trackBuild { catchError { - parallel([ - 'oss-visualRegression': { - workers.ci(name: 'oss-visualRegression', size: 's-highmem', ramDisk: true) { - kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')(1) - } - }, - 'xpack-visualRegression': { - workers.ci(name: 'xpack-visualRegression', size: 's-highmem', ramDisk: true) { - kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')(1) - } - }, - ]) + withEnv([ + 'CI_PARALLEL_PROCESS_NUMBER=1' + ]) { + parallel([ + 'oss-visualRegression': { + workers.ci(name: 'oss-visualRegression', size: 's-highmem', ramDisk: true) { + kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')() + } + }, + 'xpack-visualRegression': { + workers.ci(name: 'xpack-visualRegression', size: 's-highmem', ramDisk: true) { + kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')() + } + }, + ]) + } } kibanaPipeline.sendMail() From ea4d36f71375bf78a0313ee3adf6b426d3690fc0 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 4 Aug 2020 15:27:04 -0400 Subject: [PATCH 61/74] [CI] Fix xpack kibana build dir in xpack visual regression script --- test/scripts/jenkins_xpack_visual_regression.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_visual_regression.sh index 7fb7d7b71b2e4b..55d4a524820c57 100755 --- a/test/scripts/jenkins_xpack_visual_regression.sh +++ b/test/scripts/jenkins_xpack_visual_regression.sh @@ -7,19 +7,19 @@ echo " -> building and extracting default Kibana distributable" cd "$KIBANA_DIR" node scripts/build --debug --no-oss linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" -installDir="$PARENT_DIR/install/kibana" +installDir="$KIBANA_DIR/install/kibana" mkdir -p "$installDir" tar -xzf "$linuxBuild" -C "$installDir" --strip=1 +mkdir -p "$WORKSPACE/kibana-build-xpack" +cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/ + # cd "$KIBANA_DIR" # source "test/scripts/jenkins_xpack_page_load_metrics.sh" cd "$KIBANA_DIR" source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh" -cd "$KIBANA_DIR" -source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh" - echo " -> running visual regression tests from x-pack directory" cd "$XPACK_DIR" yarn percy exec -t 10000 -- -- \ From 8e0e228dbf281449ff17e56f3d2cc894d73ce2f3 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Tue, 4 Aug 2020 20:35:00 +0100 Subject: [PATCH 62/74] [Security Solution] Keep original note creator (#74203) * keep original note creator * unit test * fix types * typo * wording --- .../server/graphql/note/resolvers.ts | 14 ++- .../server/lib/note/saved_object.ts | 12 ++- .../timeline/routes/create_timelines_route.ts | 3 +- .../routes/import_timelines_route.test.ts | 98 ++++++++++++++++++- .../timeline/routes/utils/create_timelines.ts | 60 +++++++++--- .../timeline/routes/utils/failure_cases.ts | 1 + .../timeline/routes/utils/import_timelines.ts | 8 +- 7 files changed, 171 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts index 5f816b9ada54ed..10faa362363ad0 100644 --- a/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts @@ -81,10 +81,16 @@ export const createNoteResolvers = ( return true; }, async persistNote(root, args, { req }) { - return libs.note.persistNote(req, args.noteId || null, args.version || null, { - ...args.note, - timelineId: args.note.timelineId || null, - }); + return libs.note.persistNote( + req, + args.noteId || null, + args.version || null, + { + ...args.note, + timelineId: args.note.timelineId || null, + }, + true + ); }, }, }); diff --git a/x-pack/plugins/security_solution/server/lib/note/saved_object.ts b/x-pack/plugins/security_solution/server/lib/note/saved_object.ts index bf6090f0337f7c..0b043d4e2fdd04 100644 --- a/x-pack/plugins/security_solution/server/lib/note/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/note/saved_object.ts @@ -49,7 +49,8 @@ export interface Note { request: FrameworkRequest, noteId: string | null, version: string | null, - note: SavedNote + note: SavedNote, + overrideOwner: boolean ) => Promise; convertSavedObjectToSavedNote: ( savedObject: unknown, @@ -136,7 +137,8 @@ export const persistNote = async ( request: FrameworkRequest, noteId: string | null, version: string | null, - note: SavedNote + note: SavedNote, + overrideOwner: boolean = true ): Promise => { try { const savedObjectsClient = request.context.core.savedObjects.client; @@ -163,14 +165,14 @@ export const persistNote = async ( note: convertSavedObjectToSavedNote( await savedObjectsClient.create( noteSavedObjectType, - pickSavedNote(noteId, note, request.user) + overrideOwner ? pickSavedNote(noteId, note, request.user) : note ), timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined ), }; } - // Update new note + // Update existing note const existingNote = await getSavedNote(request, noteId); return { @@ -180,7 +182,7 @@ export const persistNote = async ( await savedObjectsClient.update( noteSavedObjectType, noteId, - pickSavedNote(noteId, note, request.user), + overrideOwner ? pickSavedNote(noteId, note, request.user) : note, { version: existingNote.version || undefined, } diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts index 5bc4bec45dfb22..7abcb390d0221c 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts @@ -20,6 +20,7 @@ import { TimelineStatusActions, } from './utils/common'; import { createTimelines } from './utils/create_timelines'; +import { DEFAULT_ERROR } from './utils/failure_cases'; export const createTimelinesRoute = ( router: IRouter, @@ -85,7 +86,7 @@ export const createTimelinesRoute = ( return siemResponse.error( compareTimelinesStatus.checkIsFailureCases(TimelineStatusActions.create) || { statusCode: 405, - body: 'update timeline error', + body: DEFAULT_ERROR, } ); } diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts index b817896e901c10..2ad6c5d6fff601 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts @@ -46,6 +46,7 @@ describe('import timelines', () => { let mockPersistTimeline: jest.Mock; let mockPersistPinnedEventOnTimeline: jest.Mock; let mockPersistNote: jest.Mock; + let mockGetNote: jest.Mock; let mockGetTupleDuplicateErrorsAndUniqueTimeline: jest.Mock; beforeEach(() => { @@ -69,6 +70,7 @@ describe('import timelines', () => { mockPersistTimeline = jest.fn(); mockPersistPinnedEventOnTimeline = jest.fn(); mockPersistNote = jest.fn(); + mockGetNote = jest.fn(); mockGetTupleDuplicateErrorsAndUniqueTimeline = jest.fn(); jest.doMock('../create_timelines_stream_from_ndjson', () => { @@ -113,6 +115,37 @@ describe('import timelines', () => { jest.doMock('../../note/saved_object', () => { return { persistNote: mockPersistNote, + getNote: mockGetNote + .mockResolvedValueOnce({ + noteId: 'd2649d40-6bc5-11ea-86f0-5db0048c6086', + version: 'WzExNjQsMV0=', + eventId: undefined, + note: 'original note', + created: '1584830796960', + createdBy: 'original author A', + updated: '1584830796960', + updatedBy: 'original author A', + }) + .mockResolvedValueOnce({ + noteId: '73ac2370-6bc2-11ea-a90b-f5341fb7a189', + version: 'WzExMjgsMV0=', + eventId: 'ZaAi8nAB5OldxqFfdhke', + note: 'original event note', + created: '1584830796960', + createdBy: 'original author B', + updated: '1584830796960', + updatedBy: 'original author B', + }) + .mockResolvedValue({ + noteId: 'f7b71620-6bc2-11ea-a0b6-33c7b2a78885', + version: 'WzExMzUsMV0=', + eventId: 'ZaAi8nAB5OldxqFfdhke', + note: 'event note2', + created: '1584830796960', + createdBy: 'angela', + updated: '1584830796960', + updatedBy: 'angela', + }), }; }); @@ -213,6 +246,14 @@ describe('import timelines', () => { ); }); + test('should Check if note exists', async () => { + const mockRequest = getImportTimelinesRequest(); + await server.inject(mockRequest, context); + expect(mockGetNote.mock.calls[0][1]).toEqual( + mockUniqueParsedObjects[0].globalNotes[0].noteId + ); + }); + test('should Create notes', async () => { const mockRequest = getImportTimelinesRequest(); await server.inject(mockRequest, context); @@ -237,20 +278,67 @@ describe('import timelines', () => { expect(mockPersistNote.mock.calls[0][2]).toEqual(mockCreatedTimeline.version); }); - test('should provide new notes when Creating notes for a timeline', async () => { + test('should provide new notes with original author info when Creating notes for a timeline', async () => { + const mockRequest = getImportTimelinesRequest(); + await server.inject(mockRequest, context); + expect(mockPersistNote.mock.calls[0][3]).toEqual({ + eventId: undefined, + note: 'original note', + created: '1584830796960', + createdBy: 'original author A', + updated: '1584830796960', + updatedBy: 'original author A', + timelineId: mockCreatedTimeline.savedObjectId, + }); + expect(mockPersistNote.mock.calls[1][3]).toEqual({ + eventId: mockUniqueParsedObjects[0].eventNotes[0].eventId, + note: 'original event note', + created: '1584830796960', + createdBy: 'original author B', + updated: '1584830796960', + updatedBy: 'original author B', + timelineId: mockCreatedTimeline.savedObjectId, + }); + expect(mockPersistNote.mock.calls[2][3]).toEqual({ + eventId: mockUniqueParsedObjects[0].eventNotes[1].eventId, + note: 'event note2', + created: '1584830796960', + createdBy: 'angela', + updated: '1584830796960', + updatedBy: 'angela', + timelineId: mockCreatedTimeline.savedObjectId, + }); + }); + + test('should keep current author if note does not exist when Creating notes for a timeline', async () => { + mockGetNote.mockReset(); + mockGetNote.mockRejectedValue(new Error()); + const mockRequest = getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][3]).toEqual({ + created: mockUniqueParsedObjects[0].globalNotes[0].created, + createdBy: mockUniqueParsedObjects[0].globalNotes[0].createdBy, + updated: mockUniqueParsedObjects[0].globalNotes[0].updated, + updatedBy: mockUniqueParsedObjects[0].globalNotes[0].updatedBy, eventId: undefined, note: mockUniqueParsedObjects[0].globalNotes[0].note, timelineId: mockCreatedTimeline.savedObjectId, }); expect(mockPersistNote.mock.calls[1][3]).toEqual({ + created: mockUniqueParsedObjects[0].eventNotes[0].created, + createdBy: mockUniqueParsedObjects[0].eventNotes[0].createdBy, + updated: mockUniqueParsedObjects[0].eventNotes[0].updated, + updatedBy: mockUniqueParsedObjects[0].eventNotes[0].updatedBy, eventId: mockUniqueParsedObjects[0].eventNotes[0].eventId, note: mockUniqueParsedObjects[0].eventNotes[0].note, timelineId: mockCreatedTimeline.savedObjectId, }); expect(mockPersistNote.mock.calls[2][3]).toEqual({ + created: mockUniqueParsedObjects[0].eventNotes[1].created, + createdBy: mockUniqueParsedObjects[0].eventNotes[1].createdBy, + updated: mockUniqueParsedObjects[0].eventNotes[1].updated, + updatedBy: mockUniqueParsedObjects[0].eventNotes[1].updatedBy, eventId: mockUniqueParsedObjects[0].eventNotes[1].eventId, note: mockUniqueParsedObjects[0].eventNotes[1].note, timelineId: mockCreatedTimeline.savedObjectId, @@ -573,6 +661,10 @@ describe('import timeline templates', () => { expect(mockPersistNote.mock.calls[0][3]).toEqual({ eventId: undefined, note: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].note, + created: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].created, + createdBy: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].createdBy, + updated: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].updated, + updatedBy: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].updatedBy, timelineId: mockCreatedTemplateTimeline.savedObjectId, }); }); @@ -721,6 +813,10 @@ describe('import timeline templates', () => { expect(mockPersistNote.mock.calls[0][3]).toEqual({ eventId: undefined, note: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].note, + created: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].created, + createdBy: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].createdBy, + updated: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].updated, + updatedBy: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].updatedBy, timelineId: mockCreatedTemplateTimeline.savedObjectId, }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts index cdedffbbd94589..6bdecb5d80ecc1 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts @@ -45,26 +45,56 @@ export const savePinnedEvents = ( ) ); -export const saveNotes = ( +const getNewNote = async ( frameworkRequest: FrameworkRequest, + note: NoteResult, timelineSavedObjectId: string, - timelineVersion?: string | null, - existingNoteIds?: string[], - newNotes?: NoteResult[] -) => { - return Promise.all( - newNotes?.map((note) => { - const newNote: SavedNote = { + overrideOwner: boolean +): Promise => { + let savedNote = note; + try { + savedNote = await noteLib.getNote(frameworkRequest, note.noteId); + // eslint-disable-next-line no-empty + } catch (e) {} + return overrideOwner + ? { eventId: note.eventId, note: note.note, timelineId: timelineSavedObjectId, + } + : { + eventId: savedNote.eventId, + note: savedNote.note, + created: savedNote.created, + createdBy: savedNote.createdBy, + updated: savedNote.updated, + updatedBy: savedNote.updatedBy, + timelineId: timelineSavedObjectId, }; +}; +export const saveNotes = async ( + frameworkRequest: FrameworkRequest, + timelineSavedObjectId: string, + timelineVersion?: string | null, + existingNoteIds?: string[], + newNotes?: NoteResult[], + overrideOwner: boolean = true +) => { + return Promise.all( + newNotes?.map(async (note) => { + const newNote = await getNewNote( + frameworkRequest, + note, + timelineSavedObjectId, + overrideOwner + ); return noteLib.persistNote( frameworkRequest, - existingNoteIds?.find((nId) => nId === note.noteId) ?? null, + overrideOwner ? existingNoteIds?.find((nId) => nId === note.noteId) ?? null : null, timelineVersion ?? null, - newNote + newNote, + overrideOwner ); }) ?? [] ); @@ -75,12 +105,18 @@ interface CreateTimelineProps { timeline: SavedTimeline; timelineSavedObjectId?: string | null; timelineVersion?: string | null; + overrideNotesOwner?: boolean; pinnedEventIds?: string[] | null; notes?: NoteResult[]; existingNoteIds?: string[]; isImmutable?: boolean; } +/** allow overrideNotesOwner means overriding by current username, + * disallow overrideNotesOwner means keep the original username. + * overrideNotesOwner = false only happens when import timeline templates, + * as we want to keep the original creator for notes + **/ export const createTimelines = async ({ frameworkRequest, timeline, @@ -90,6 +126,7 @@ export const createTimelines = async ({ notes = [], existingNoteIds = [], isImmutable, + overrideNotesOwner = true, }: CreateTimelineProps): Promise => { const responseTimeline = await saveTimelines( frameworkRequest, @@ -119,7 +156,8 @@ export const createTimelines = async ({ timelineSavedObjectId ?? newTimelineSavedObjectId, newTimelineVersion, existingNoteIds, - notes + notes, + overrideNotesOwner ), ]; } diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts index b926819d66c92b..e358ad9dbb57d6 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts @@ -37,6 +37,7 @@ export const NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE = 'You cannot convert a Timeline template to a timeline, or a timeline to a Timeline template.'; export const UPDAT_TIMELINE_VIA_IMPORT_NOT_ALLOWED_ERROR_MESSAGE = 'You cannot update a timeline via imports. Use the UI to modify existing timelines.'; +export const DEFAULT_ERROR = `Something has gone wrong. We didn't handle something properly. To help us fix this, please upload your file to https://discuss.elastic.co/c/security/siem.`; const isUpdatingStatus = ( isHandlingTemplateTimeline: boolean, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts index 1fea11f01bcc52..f62f02cc7bba94 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts @@ -26,6 +26,7 @@ import { createPromiseFromStreams } from '../../../../../../../../src/legacy/uti import { getTupleDuplicateErrorsAndUniqueTimeline } from './get_timelines_from_stream'; import { CompareTimelinesStatus } from './compare_timelines_status'; import { TimelineStatusActions } from './common'; +import { DEFAULT_ERROR } from './failure_cases'; export type ImportedTimeline = SavedTimeline & { savedObjectId: string | null; @@ -96,7 +97,6 @@ export const setTimeline = ( }; const CHUNK_PARSED_OBJECT_SIZE = 10; -const DEFAULT_IMPORT_ERROR = `Something has gone wrong. We didn't handle something properly. To help us fix this, please upload your file to https://discuss.elastic.co/c/security/siem.`; export const importTimelines = async ( file: Readable, @@ -173,6 +173,7 @@ export const importTimelines = async ( pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds, notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes], isImmutable, + overrideNotesOwner: false, }); resolve({ @@ -186,7 +187,7 @@ export const importTimelines = async ( const errorMessage = compareTimelinesStatus.checkIsFailureCases( TimelineStatusActions.createViaImport ); - const message = errorMessage?.body ?? DEFAULT_IMPORT_ERROR; + const message = errorMessage?.body ?? DEFAULT_ERROR; resolve( createBulkErrorObject({ @@ -206,6 +207,7 @@ export const importTimelines = async ( notes: globalNotes, existingNoteIds: compareTimelinesStatus.timelineInput.data?.noteIds, isImmutable, + overrideNotesOwner: false, }); resolve({ @@ -218,7 +220,7 @@ export const importTimelines = async ( TimelineStatusActions.updateViaImport ); - const message = errorMessage?.body ?? DEFAULT_IMPORT_ERROR; + const message = errorMessage?.body ?? DEFAULT_ERROR; resolve( createBulkErrorObject({ From 54e13ad6f69dfcfa674ab297a4a2fcad7c328e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Tue, 4 Aug 2020 15:50:17 -0400 Subject: [PATCH 63/74] Make the actions plugin support generics (#71439) * Initial attempt at making the actions plugin support generics * Export WebhookMethods * Fix typings for registry * Usage of Record * Apply feedback from Gidi * Cleanup * Fix validate_with_schema * Cleanup pt2 * Fix failing tests * Add generics to ActionType for ActionTypeExecutorResult Co-authored-by: Elastic Machine --- .../server/action_type_registry.test.ts | 10 +- .../actions/server/action_type_registry.ts | 26 +++- .../actions/server/actions_client.test.ts | 2 +- .../plugins/actions/server/actions_client.ts | 2 +- .../server/builtin_action_types/case/utils.ts | 18 ++- .../server/builtin_action_types/email.test.ts | 17 ++- .../server/builtin_action_types/email.ts | 29 +++-- .../builtin_action_types/es_index.test.ts | 22 ++-- .../server/builtin_action_types/es_index.ts | 17 ++- .../builtin_action_types/pagerduty.test.ts | 51 +++++--- .../server/builtin_action_types/pagerduty.ts | 24 +++- .../builtin_action_types/server_log.test.ts | 15 ++- .../server/builtin_action_types/server_log.ts | 15 ++- .../builtin_action_types/servicenow/index.ts | 31 +++-- .../server/builtin_action_types/slack.test.ts | 19 ++- .../server/builtin_action_types/slack.ts | 32 +++-- .../builtin_action_types/webhook.test.ts | 52 +++++--- .../server/builtin_action_types/webhook.ts | 43 ++++--- .../actions/server/lib/action_executor.ts | 6 +- .../actions/server/lib/license_state.test.ts | 8 +- .../actions/server/lib/task_runner_factory.ts | 2 +- .../server/lib/validate_with_schema.test.ts | 2 +- .../server/lib/validate_with_schema.ts | 32 ++++- x-pack/plugins/actions/server/plugin.test.ts | 4 +- x-pack/plugins/actions/server/plugin.ts | 25 +++- .../actions/server/routes/execute.test.ts | 4 +- .../plugins/actions/server/routes/execute.ts | 2 +- x-pack/plugins/actions/server/types.ts | 67 +++++----- .../public/cases/containers/api.ts | 19 ++- .../plugins/alerts/server/action_types.ts | 117 +++++++++++------- 30 files changed, 466 insertions(+), 247 deletions(-) diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index ce5c1fe8500fbe..b25e33400df5d0 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -41,7 +41,7 @@ beforeEach(() => { }; }); -const executor: ExecutorType = async (options) => { +const executor: ExecutorType<{}, {}, {}, void> = async (options) => { return { status: 'ok', actionId: options.actionId }; }; @@ -203,7 +203,9 @@ describe('isActionTypeEnabled', () => { id: 'foo', name: 'Foo', minimumLicenseRequired: 'basic', - executor: async () => {}, + executor: async (options) => { + return { status: 'ok', actionId: options.actionId }; + }, }; beforeEach(() => { @@ -258,7 +260,9 @@ describe('ensureActionTypeEnabled', () => { id: 'foo', name: 'Foo', minimumLicenseRequired: 'basic', - executor: async () => {}, + executor: async (options) => { + return { status: 'ok', actionId: options.actionId }; + }, }; beforeEach(() => { diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts index 1f7409fedd2c2c..4015381ff9502f 100644 --- a/x-pack/plugins/actions/server/action_type_registry.ts +++ b/x-pack/plugins/actions/server/action_type_registry.ts @@ -8,9 +8,15 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import { RunContext, TaskManagerSetupContract } from '../../task_manager/server'; import { ExecutorError, TaskRunnerFactory, ILicenseState } from './lib'; -import { ActionType, PreConfiguredAction } from './types'; import { ActionType as CommonActionType } from '../common'; import { ActionsConfigurationUtilities } from './actions_config'; +import { + ActionType, + PreConfiguredAction, + ActionTypeConfig, + ActionTypeSecrets, + ActionTypeParams, +} from './types'; export interface ActionTypeRegistryOpts { taskManager: TaskManagerSetupContract; @@ -77,7 +83,12 @@ export class ActionTypeRegistry { /** * Registers an action type to the action type registry */ - public register(actionType: ActionType) { + public register< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void + >(actionType: ActionType) { if (this.has(actionType.id)) { throw new Error( i18n.translate( @@ -91,7 +102,7 @@ export class ActionTypeRegistry { ) ); } - this.actionTypes.set(actionType.id, { ...actionType }); + this.actionTypes.set(actionType.id, { ...actionType } as ActionType); this.taskManager.registerTaskDefinitions({ [`actions:${actionType.id}`]: { title: actionType.name, @@ -112,7 +123,12 @@ export class ActionTypeRegistry { /** * Returns an action type, throws if not registered */ - public get(id: string): ActionType { + public get< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void + >(id: string): ActionType { if (!this.has(id)) { throw Boom.badRequest( i18n.translate('xpack.actions.actionTypeRegistry.get.missingActionTypeErrorMessage', { @@ -123,7 +139,7 @@ export class ActionTypeRegistry { }) ); } - return this.actionTypes.get(id)!; + return this.actionTypes.get(id)! as ActionType; } /** diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 90b989ac3b52eb..16a5a59882dd6c 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -39,7 +39,7 @@ let actionsClient: ActionsClient; let mockedLicenseState: jest.Mocked; let actionTypeRegistry: ActionTypeRegistry; let actionTypeRegistryParams: ActionTypeRegistryOpts; -const executor: ExecutorType = async (options) => { +const executor: ExecutorType<{}, {}, {}, void> = async (options) => { return { status: 'ok', actionId: options.actionId }; }; diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 6744a8d1116234..d46ad3e2e24238 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -298,7 +298,7 @@ export class ActionsClient { public async execute({ actionId, params, - }: Omit): Promise { + }: Omit): Promise> { await this.authorization.ensureAuthorized('execute'); return this.actionExecutor.execute({ actionId, params, request: this.request }); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts index 676a4776d00552..82dedb09c429e1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts @@ -10,6 +10,10 @@ import { schema } from '@kbn/config-schema'; import { ActionTypeExecutorOptions, ActionTypeExecutorResult, ActionType } from '../../types'; import { ExecutorParamsSchema } from './schema'; +import { + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, +} from './types'; import { CreateExternalServiceArgs, @@ -23,6 +27,7 @@ import { TransformFieldsArgs, Comment, ExecutorSubActionPushParams, + PushToServiceResponse, } from './types'; import { transformers } from './transformers'; @@ -63,14 +68,17 @@ export const createConnectorExecutor = ({ api, createExternalService, }: CreateExternalServiceBasicArgs) => async ( - execOptions: ActionTypeExecutorOptions -): Promise => { + execOptions: ActionTypeExecutorOptions< + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, + ExecutorParams + > +): Promise> => { const { actionId, config, params, secrets } = execOptions; - const { subAction, subActionParams } = params as ExecutorParams; + const { subAction, subActionParams } = params; let data = {}; - const res: Pick & - Pick = { + const res: ActionTypeExecutorResult = { status: 'ok', actionId, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 1a24622e1cabbd..195f6db538ae59 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -10,7 +10,6 @@ jest.mock('./lib/send_email', () => ({ import { Logger } from '../../../../../src/core/server'; -import { ActionType, ActionTypeExecutorOptions } from '../types'; import { actionsConfigMock } from '../actions_config.mock'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { createActionTypeRegistry } from './index.test'; @@ -21,6 +20,8 @@ import { ActionTypeConfigType, ActionTypeSecretsType, getActionType, + EmailActionType, + EmailActionTypeExecutorOptions, } from './email'; const sendEmailMock = sendEmail as jest.Mock; @@ -29,13 +30,17 @@ const ACTION_TYPE_ID = '.email'; const services = actionsMock.createServices(); -let actionType: ActionType; +let actionType: EmailActionType; let mockedLogger: jest.Mocked; beforeEach(() => { jest.resetAllMocks(); const { actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + actionType = actionTypeRegistry.get< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType + >(ACTION_TYPE_ID); }); describe('actionTypeRegistry.get() works', () => { @@ -242,7 +247,7 @@ describe('execute()', () => { }; const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: EmailActionTypeExecutorOptions = { actionId, config, params, @@ -306,7 +311,7 @@ describe('execute()', () => { }; const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: EmailActionTypeExecutorOptions = { actionId, config, params, @@ -363,7 +368,7 @@ describe('execute()', () => { }; const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: EmailActionTypeExecutorOptions = { actionId, config, params, diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index 7ddb123a4d7805..a51a0432a01e0f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -15,6 +15,18 @@ import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; +export type EmailActionType = ActionType< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType, + unknown +>; +export type EmailActionTypeExecutorOptions = ActionTypeExecutorOptions< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType +>; + // config definition export type ActionTypeConfigType = TypeOf; @@ -30,10 +42,9 @@ const ConfigSchema = schema.object(ConfigSchemaProps); function validateConfig( configurationUtilities: ActionsConfigurationUtilities, - configObject: unknown + configObject: ActionTypeConfigType ): string | void { - // avoids circular reference ... - const config = configObject as ActionTypeConfigType; + const config = configObject; // Make sure service is set, or if not, both host/port must be set. // If service is set, host/port are ignored, when the email is sent. @@ -113,7 +124,7 @@ interface GetActionTypeParams { } // action type definition -export function getActionType(params: GetActionTypeParams): ActionType { +export function getActionType(params: GetActionTypeParams): EmailActionType { const { logger, configurationUtilities } = params; return { id: '.email', @@ -136,12 +147,12 @@ export function getActionType(params: GetActionTypeParams): ActionType { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: EmailActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const config = execOptions.config as ActionTypeConfigType; - const secrets = execOptions.secrets as ActionTypeSecretsType; - const params = execOptions.params as ActionParamsType; + const config = execOptions.config; + const secrets = execOptions.secrets; + const params = execOptions.params; const transport: Transport = {}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts index be60f4c2f28afd..7a0e24521a1c6e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts @@ -8,21 +8,25 @@ jest.mock('./lib/send_email', () => ({ sendEmail: jest.fn(), })); -import { ActionType, ActionTypeExecutorOptions } from '../types'; import { validateConfig, validateParams } from '../lib'; import { createActionTypeRegistry } from './index.test'; -import { ActionParamsType, ActionTypeConfigType } from './es_index'; import { actionsMock } from '../mocks'; +import { + ActionParamsType, + ActionTypeConfigType, + ESIndexActionType, + ESIndexActionTypeExecutorOptions, +} from './es_index'; const ACTION_TYPE_ID = '.index'; const services = actionsMock.createServices(); -let actionType: ActionType; +let actionType: ESIndexActionType; beforeAll(() => { const { actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + actionType = actionTypeRegistry.get(ACTION_TYPE_ID); }); beforeEach(() => { @@ -144,12 +148,12 @@ describe('params validation', () => { describe('execute()', () => { test('ensure parameters are as expected', async () => { const secrets = {}; - let config: Partial; + let config: ActionTypeConfigType; let params: ActionParamsType; - let executorOptions: ActionTypeExecutorOptions; + let executorOptions: ESIndexActionTypeExecutorOptions; // minimal params - config = { index: 'index-value', refresh: false }; + config = { index: 'index-value', refresh: false, executionTimeField: null }; params = { documents: [{ jim: 'bob' }], }; @@ -215,7 +219,7 @@ describe('execute()', () => { `); // minimal params - config = { index: 'index-value', executionTimeField: undefined, refresh: false }; + config = { index: 'index-value', executionTimeField: null, refresh: false }; params = { documents: [{ jim: 'bob' }], }; @@ -245,7 +249,7 @@ describe('execute()', () => { `); // multiple documents - config = { index: 'index-value', executionTimeField: undefined, refresh: false }; + config = { index: 'index-value', executionTimeField: null, refresh: false }; params = { documents: [{ a: 1 }, { b: 2 }], }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts index 899684367d52d3..53bf75651b1e5b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts @@ -11,6 +11,13 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; +export type ESIndexActionType = ActionType; +export type ESIndexActionTypeExecutorOptions = ActionTypeExecutorOptions< + ActionTypeConfigType, + {}, + ActionParamsType +>; + // config definition export type ActionTypeConfigType = TypeOf; @@ -33,7 +40,7 @@ const ParamsSchema = schema.object({ }); // action type definition -export function getActionType({ logger }: { logger: Logger }): ActionType { +export function getActionType({ logger }: { logger: Logger }): ESIndexActionType { return { id: '.index', minimumLicenseRequired: 'basic', @@ -52,11 +59,11 @@ export function getActionType({ logger }: { logger: Logger }): ActionType { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: ESIndexActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const config = execOptions.config as ActionTypeConfigType; - const params = execOptions.params as ActionParamsType; + const config = execOptions.config; + const params = execOptions.params; const services = execOptions.services; const index = config.index; diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts index b1ed3728edfaea..c379c05ee88e38 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -8,14 +8,21 @@ jest.mock('./lib/post_pagerduty', () => ({ postPagerduty: jest.fn(), })); -import { getActionType } from './pagerduty'; -import { ActionType, Services, ActionTypeExecutorOptions } from '../types'; +import { Services } from '../types'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { postPagerduty } from './lib/post_pagerduty'; import { createActionTypeRegistry } from './index.test'; import { Logger } from '../../../../../src/core/server'; import { actionsConfigMock } from '../actions_config.mock'; import { actionsMock } from '../mocks'; +import { + ActionParamsType, + ActionTypeConfigType, + ActionTypeSecretsType, + getActionType, + PagerDutyActionType, + PagerDutyActionTypeExecutorOptions, +} from './pagerduty'; const postPagerdutyMock = postPagerduty as jest.Mock; @@ -23,12 +30,16 @@ const ACTION_TYPE_ID = '.pagerduty'; const services: Services = actionsMock.createServices(); -let actionType: ActionType; +let actionType: PagerDutyActionType; let mockedLogger: jest.Mocked; beforeAll(() => { const { logger, actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + actionType = actionTypeRegistry.get< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType + >(ACTION_TYPE_ID); mockedLogger = logger; }); @@ -167,7 +178,7 @@ describe('execute()', () => { test('should succeed with minimal valid params', async () => { const secrets = { routingKey: 'super-secret' }; - const config = {}; + const config = { apiUrl: null }; const params = {}; postPagerdutyMock.mockImplementation(() => { @@ -175,7 +186,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -219,7 +230,7 @@ describe('execute()', () => { const config = { apiUrl: 'the-api-url', }; - const params = { + const params: ActionParamsType = { eventAction: 'trigger', dedupKey: 'a-dedup-key', summary: 'the summary', @@ -236,7 +247,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -284,7 +295,7 @@ describe('execute()', () => { const config = { apiUrl: 'the-api-url', }; - const params = { + const params: ActionParamsType = { eventAction: 'acknowledge', dedupKey: 'a-dedup-key', summary: 'the summary', @@ -301,7 +312,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -340,7 +351,7 @@ describe('execute()', () => { const config = { apiUrl: 'the-api-url', }; - const params = { + const params: ActionParamsType = { eventAction: 'resolve', dedupKey: 'a-dedup-key', summary: 'the summary', @@ -357,7 +368,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -390,7 +401,7 @@ describe('execute()', () => { test('should fail when sendPagerdury throws', async () => { const secrets = { routingKey: 'super-secret' }; - const config = {}; + const config = { apiUrl: null }; const params = {}; postPagerdutyMock.mockImplementation(() => { @@ -398,7 +409,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -418,7 +429,7 @@ describe('execute()', () => { test('should fail when sendPagerdury returns 429', async () => { const secrets = { routingKey: 'super-secret' }; - const config = {}; + const config = { apiUrl: null }; const params = {}; postPagerdutyMock.mockImplementation(() => { @@ -426,7 +437,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -446,7 +457,7 @@ describe('execute()', () => { test('should fail when sendPagerdury returns 501', async () => { const secrets = { routingKey: 'super-secret' }; - const config = {}; + const config = { apiUrl: null }; const params = {}; postPagerdutyMock.mockImplementation(() => { @@ -454,7 +465,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, @@ -474,7 +485,7 @@ describe('execute()', () => { test('should fail when sendPagerdury returns 418', async () => { const secrets = { routingKey: 'super-secret' }; - const config = {}; + const config = { apiUrl: null }; const params = {}; postPagerdutyMock.mockImplementation(() => { @@ -482,7 +493,7 @@ describe('execute()', () => { }); const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { + const executorOptions: PagerDutyActionTypeExecutorOptions = { actionId, config, params, diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts index 0c8802060164d3..b76e57419bc566 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -16,6 +16,18 @@ import { ActionsConfigurationUtilities } from '../actions_config'; // https://v2.developer.pagerduty.com/docs/events-api-v2 const PAGER_DUTY_API_URL = 'https://events.pagerduty.com/v2/enqueue'; +export type PagerDutyActionType = ActionType< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType, + unknown +>; +export type PagerDutyActionTypeExecutorOptions = ActionTypeExecutorOptions< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType +>; + // config definition export type ActionTypeConfigType = TypeOf; @@ -100,7 +112,7 @@ export function getActionType({ }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; -}): ActionType { +}): PagerDutyActionType { return { id: '.pagerduty', minimumLicenseRequired: 'gold', @@ -142,12 +154,12 @@ function getPagerDutyApiUrl(config: ActionTypeConfigType): string { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: PagerDutyActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const config = execOptions.config as ActionTypeConfigType; - const secrets = execOptions.secrets as ActionTypeSecretsType; - const params = execOptions.params as ActionParamsType; + const config = execOptions.config; + const secrets = execOptions.secrets; + const params = execOptions.params; const services = execOptions.services; const apiUrl = getPagerDutyApiUrl(config); diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts index d5a9c0cc1ccd2a..e4828f33765ce1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts @@ -4,20 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionType } from '../types'; import { validateParams } from '../lib'; import { Logger } from '../../../../../src/core/server'; import { createActionTypeRegistry } from './index.test'; import { actionsMock } from '../mocks'; +import { + ActionParamsType, + ServerLogActionType, + ServerLogActionTypeExecutorOptions, +} from './server_log'; const ACTION_TYPE_ID = '.server-log'; -let actionType: ActionType; +let actionType: ServerLogActionType; let mockedLogger: jest.Mocked; beforeAll(() => { const { logger, actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + actionType = actionTypeRegistry.get<{}, {}, ActionParamsType>(ACTION_TYPE_ID); mockedLogger = logger; expect(actionType).toBeTruthy(); }); @@ -88,13 +92,14 @@ describe('validateParams()', () => { describe('execute()', () => { test('calls the executor with proper params', async () => { const actionId = 'some-id'; - await actionType.executor({ + const executorOptions: ServerLogActionTypeExecutorOptions = { actionId, services: actionsMock.createServices(), params: { message: 'message text here', level: 'info' }, config: {}, secrets: {}, - }); + }; + await actionType.executor(executorOptions); expect(mockedLogger.info).toHaveBeenCalledWith('Server log: message text here'); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts index bf8a3d8032cc58..490764fb16bfdb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts @@ -12,6 +12,13 @@ import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; import { withoutControlCharacters } from './lib/string_utils'; +export type ServerLogActionType = ActionType<{}, {}, ActionParamsType>; +export type ServerLogActionTypeExecutorOptions = ActionTypeExecutorOptions< + {}, + {}, + ActionParamsType +>; + // params definition export type ActionParamsType = TypeOf; @@ -32,7 +39,7 @@ const ParamsSchema = schema.object({ }); // action type definition -export function getActionType({ logger }: { logger: Logger }): ActionType { +export function getActionType({ logger }: { logger: Logger }): ServerLogActionType { return { id: '.server-log', minimumLicenseRequired: 'basic', @@ -50,10 +57,10 @@ export function getActionType({ logger }: { logger: Logger }): ActionType { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: ServerLogActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const params = execOptions.params as ActionParamsType; + const params = execOptions.params; const sanitizedMessage = withoutControlCharacters(params.message); try { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index e62ca465f30f86..109008b8fc9fb4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -17,9 +17,14 @@ import { ActionsConfigurationUtilities } from '../../actions_config'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; import { createExternalService } from './service'; import { api } from './api'; -import { ExecutorParams, ExecutorSubActionPushParams } from './types'; import * as i18n from './translations'; import { Logger } from '../../../../../../src/core/server'; +import { + ExecutorParams, + ExecutorSubActionPushParams, + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, +} from './types'; // TODO: to remove, need to support Case import { buildMap, mapParams } from '../case/utils'; @@ -31,7 +36,14 @@ interface GetActionTypeParams { } // action type definition -export function getActionType(params: GetActionTypeParams): ActionType { +export function getActionType( + params: GetActionTypeParams +): ActionType< + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + ExecutorParams, + PushToServiceResponse | {} +> { const { logger, configurationUtilities } = params; return { id: '.servicenow', @@ -54,10 +66,14 @@ export function getActionType(params: GetActionTypeParams): ActionType { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: ActionTypeExecutorOptions< + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + ExecutorParams + > +): Promise> { const { actionId, config, params, secrets } = execOptions; - const { subAction, subActionParams } = params as ExecutorParams; + const { subAction, subActionParams } = params; let data: PushToServiceResponse | null = null; const externalService = createExternalService({ @@ -81,9 +97,8 @@ async function executor( const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; const { comments, externalId, ...restParams } = pushToServiceParams; - const mapping = config.incidentConfiguration - ? buildMap(config.incidentConfiguration.mapping) - : null; + const incidentConfiguration = config.incidentConfiguration; + const mapping = incidentConfiguration ? buildMap(incidentConfiguration.mapping) : null; const externalObject = config.incidentConfiguration && mapping ? mapParams(restParams, mapping) : {}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index d1a739c2304f23..6d4176067c3ba0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -4,14 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ActionType, - Services, - ActionTypeExecutorOptions, - ActionTypeExecutorResult, -} from '../types'; +import { Services, ActionTypeExecutorResult } from '../types'; import { validateParams, validateSecrets } from '../lib'; -import { getActionType } from './slack'; +import { getActionType, SlackActionType, SlackActionTypeExecutorOptions } from './slack'; import { actionsConfigMock } from '../actions_config.mock'; import { actionsMock } from '../mocks'; @@ -19,11 +14,13 @@ const ACTION_TYPE_ID = '.slack'; const services: Services = actionsMock.createServices(); -let actionType: ActionType; +let actionType: SlackActionType; beforeAll(() => { actionType = getActionType({ - async executor() {}, + async executor(options) { + return { status: 'ok', actionId: options.actionId }; + }, configurationUtilities: actionsConfigMock.create(), }); }); @@ -119,7 +116,7 @@ describe('validateActionTypeSecrets()', () => { describe('execute()', () => { beforeAll(() => { - async function mockSlackExecutor(options: ActionTypeExecutorOptions) { + async function mockSlackExecutor(options: SlackActionTypeExecutorOptions) { const { params } = options; const { message } = params; if (message == null) throw new Error('message property required in parameter'); @@ -134,7 +131,7 @@ describe('execute()', () => { text: `slack mockExecutor success: ${message}`, actionId: '', status: 'ok', - } as ActionTypeExecutorResult; + } as ActionTypeExecutorResult; } actionType = getActionType({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index 55c373f14cd695..209582585256b4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -21,6 +21,13 @@ import { } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; +export type SlackActionType = ActionType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; +export type SlackActionTypeExecutorOptions = ActionTypeExecutorOptions< + {}, + ActionTypeSecretsType, + ActionParamsType +>; + // secrets definition export type ActionTypeSecretsType = TypeOf; @@ -46,8 +53,8 @@ export function getActionType({ executor = slackExecutor, }: { configurationUtilities: ActionsConfigurationUtilities; - executor?: ExecutorType; -}): ActionType { + executor?: ExecutorType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; +}): SlackActionType { return { id: '.slack', minimumLicenseRequired: 'gold', @@ -92,11 +99,11 @@ function valdiateActionTypeConfig( // action executor async function slackExecutor( - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: SlackActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const secrets = execOptions.secrets as ActionTypeSecretsType; - const params = execOptions.params as ActionParamsType; + const secrets = execOptions.secrets; + const params = execOptions.params; let result: IncomingWebhookResult; const { webhookUrl } = secrets; @@ -156,18 +163,21 @@ async function slackExecutor( return successResult(actionId, result); } -function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { +function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { return { status: 'ok', data, actionId }; } -function errorResult(actionId: string, message: string): ActionTypeExecutorResult { +function errorResult(actionId: string, message: string): ActionTypeExecutorResult { return { status: 'error', message, actionId, }; } -function serviceErrorResult(actionId: string, serviceMessage: string): ActionTypeExecutorResult { +function serviceErrorResult( + actionId: string, + serviceMessage: string +): ActionTypeExecutorResult { const errMessage = i18n.translate('xpack.actions.builtin.slack.errorPostingErrorMessage', { defaultMessage: 'error posting slack message', }); @@ -179,7 +189,7 @@ function serviceErrorResult(actionId: string, serviceMessage: string): ActionTyp }; } -function retryResult(actionId: string, message: string): ActionTypeExecutorResult { +function retryResult(actionId: string, message: string): ActionTypeExecutorResult { const errMessage = i18n.translate( 'xpack.actions.builtin.slack.errorPostingRetryLaterErrorMessage', { @@ -198,7 +208,7 @@ function retryResultSeconds( actionId: string, message: string, retryAfter: number -): ActionTypeExecutorResult { +): ActionTypeExecutorResult { const retryEpoch = Date.now() + retryAfter * 1000; const retry = new Date(retryEpoch); const retryString = retry.toISOString(); diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 53b17f58d6e187..26dd8a1a1402a0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -8,14 +8,21 @@ jest.mock('axios', () => ({ request: jest.fn(), })); -import { getActionType } from './webhook'; -import { ActionType, Services } from '../types'; +import { Services } from '../types'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { actionsConfigMock } from '../actions_config.mock'; import { createActionTypeRegistry } from './index.test'; import { Logger } from '../../../../../src/core/server'; import { actionsMock } from '../mocks'; import axios from 'axios'; +import { + ActionParamsType, + ActionTypeConfigType, + ActionTypeSecretsType, + getActionType, + WebhookActionType, + WebhookMethods, +} from './webhook'; const axiosRequestMock = axios.request as jest.Mock; @@ -23,12 +30,16 @@ const ACTION_TYPE_ID = '.webhook'; const services: Services = actionsMock.createServices(); -let actionType: ActionType; +let actionType: WebhookActionType; let mockedLogger: jest.Mocked; beforeAll(() => { const { logger, actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + actionType = actionTypeRegistry.get< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType + >(ACTION_TYPE_ID); mockedLogger = logger; }); @@ -235,16 +246,17 @@ describe('execute()', () => { }); test('execute with username/password sends request with basic auth', async () => { + const config: ActionTypeConfigType = { + url: 'https://abc.def/my-webhook', + method: WebhookMethods.POST, + headers: { + aheader: 'a value', + }, + }; await actionType.executor({ actionId: 'some-id', services, - config: { - url: 'https://abc.def/my-webhook', - method: 'post', - headers: { - aheader: 'a value', - }, - }, + config, secrets: { user: 'abc', password: '123' }, params: { body: 'some data' }, }); @@ -266,17 +278,19 @@ describe('execute()', () => { }); test('execute without username/password sends request without basic auth', async () => { + const config: ActionTypeConfigType = { + url: 'https://abc.def/my-webhook', + method: WebhookMethods.POST, + headers: { + aheader: 'a value', + }, + }; + const secrets: ActionTypeSecretsType = { user: null, password: null }; await actionType.executor({ actionId: 'some-id', services, - config: { - url: 'https://abc.def/my-webhook', - method: 'post', - headers: { - aheader: 'a value', - }, - }, - secrets: {}, + config, + secrets, params: { body: 'some data' }, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index 0b8b27b2789286..be75742fa882ee 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -17,11 +17,23 @@ import { ActionsConfigurationUtilities } from '../actions_config'; import { Logger } from '../../../../../src/core/server'; // config definition -enum WebhookMethods { +export enum WebhookMethods { POST = 'post', PUT = 'put', } +export type WebhookActionType = ActionType< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType, + unknown +>; +export type WebhookActionTypeExecutorOptions = ActionTypeExecutorOptions< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType +>; + const HeadersSchema = schema.recordOf(schema.string(), schema.string()); const configSchemaProps = { url: schema.string(), @@ -31,7 +43,7 @@ const configSchemaProps = { headers: nullableType(HeadersSchema), }; const ConfigSchema = schema.object(configSchemaProps); -type ActionTypeConfigType = TypeOf; +export type ActionTypeConfigType = TypeOf; // secrets definition export type ActionTypeSecretsType = TypeOf; @@ -51,7 +63,7 @@ const SecretsSchema = schema.object(secretSchemaProps, { }); // params definition -type ActionParamsType = TypeOf; +export type ActionParamsType = TypeOf; const ParamsSchema = schema.object({ body: schema.maybe(schema.string()), }); @@ -63,7 +75,7 @@ export function getActionType({ }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; -}): ActionType { +}): WebhookActionType { return { id: '.webhook', minimumLicenseRequired: 'gold', @@ -112,13 +124,13 @@ function validateActionTypeConfig( // action executor export async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: WebhookActionTypeExecutorOptions +): Promise> { const actionId = execOptions.actionId; - const { method, url, headers = {} } = execOptions.config as ActionTypeConfigType; - const { body: data } = execOptions.params as ActionParamsType; + const { method, url, headers = {} } = execOptions.config; + const { body: data } = execOptions.params; - const secrets: ActionTypeSecretsType = execOptions.secrets as ActionTypeSecretsType; + const secrets: ActionTypeSecretsType = execOptions.secrets; const basicAuth = isString(secrets.user) && isString(secrets.password) ? { auth: { username: secrets.user, password: secrets.password } } @@ -172,11 +184,14 @@ export async function executor( } // Action Executor Result w/ internationalisation -function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { +function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { return { status: 'ok', data, actionId }; } -function errorResultInvalid(actionId: string, serviceMessage: string): ActionTypeExecutorResult { +function errorResultInvalid( + actionId: string, + serviceMessage: string +): ActionTypeExecutorResult { const errMessage = i18n.translate('xpack.actions.builtin.webhook.invalidResponseErrorMessage', { defaultMessage: 'error calling webhook, invalid response', }); @@ -188,7 +203,7 @@ function errorResultInvalid(actionId: string, serviceMessage: string): ActionTyp }; } -function errorResultUnexpectedError(actionId: string): ActionTypeExecutorResult { +function errorResultUnexpectedError(actionId: string): ActionTypeExecutorResult { const errMessage = i18n.translate('xpack.actions.builtin.webhook.unreachableErrorMessage', { defaultMessage: 'error calling webhook, unexpected error', }); @@ -199,7 +214,7 @@ function errorResultUnexpectedError(actionId: string): ActionTypeExecutorResult }; } -function retryResult(actionId: string, serviceMessage: string): ActionTypeExecutorResult { +function retryResult(actionId: string, serviceMessage: string): ActionTypeExecutorResult { const errMessage = i18n.translate( 'xpack.actions.builtin.webhook.invalidResponseRetryLaterErrorMessage', { @@ -220,7 +235,7 @@ function retryResultSeconds( serviceMessage: string, retryAfter: number -): ActionTypeExecutorResult { +): ActionTypeExecutorResult { const retryEpoch = Date.now() + retryAfter * 1000; const retry = new Date(retryEpoch); const retryString = retry.toISOString(); diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 0e63cc8f5956e9..bce06c829b1bcc 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -59,7 +59,7 @@ export class ActionExecutor { actionId, params, request, - }: ExecuteOptions): Promise { + }: ExecuteOptions): Promise> { if (!this.isInitialized) { throw new Error('ActionExecutor not initialized'); } @@ -125,7 +125,7 @@ export class ActionExecutor { }; eventLogger.startTiming(event); - let rawResult: ActionTypeExecutorResult | null | undefined | void; + let rawResult: ActionTypeExecutorResult; try { rawResult = await actionType.executor({ actionId, @@ -173,7 +173,7 @@ export class ActionExecutor { } } -function actionErrorToMessage(result: ActionTypeExecutorResult): string { +function actionErrorToMessage(result: ActionTypeExecutorResult): string { let message = result.message || 'unknown error running action'; if (result.serviceMessage) { diff --git a/x-pack/plugins/actions/server/lib/license_state.test.ts b/x-pack/plugins/actions/server/lib/license_state.test.ts index 0a474ec3ae3ea3..32c3c54faf0072 100644 --- a/x-pack/plugins/actions/server/lib/license_state.test.ts +++ b/x-pack/plugins/actions/server/lib/license_state.test.ts @@ -59,7 +59,9 @@ describe('isLicenseValidForActionType', () => { id: 'foo', name: 'Foo', minimumLicenseRequired: 'gold', - executor: async () => {}, + executor: async (options) => { + return { status: 'ok', actionId: options.actionId }; + }, }; beforeEach(() => { @@ -120,7 +122,9 @@ describe('ensureLicenseForActionType()', () => { id: 'foo', name: 'Foo', minimumLicenseRequired: 'gold', - executor: async () => {}, + executor: async (options) => { + return { status: 'ok', actionId: options.actionId }; + }, }; beforeEach(() => { diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index 9204c41b9288c2..10a8501e856d26 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -94,7 +94,7 @@ export class TaskRunnerFactory { }, } as unknown) as KibanaRequest; - let executorResult: ActionTypeExecutorResult; + let executorResult: ActionTypeExecutorResult; try { executorResult = await actionExecutor.execute({ params, diff --git a/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts b/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts index 03ae7a9b35a813..10c688c075eaba 100644 --- a/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts +++ b/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; import { ActionType, ExecutorType } from '../types'; -const executor: ExecutorType = async (options) => { +const executor: ExecutorType<{}, {}, {}, void> = async (options) => { return { status: 'ok', actionId: options.actionId }; }; diff --git a/x-pack/plugins/actions/server/lib/validate_with_schema.ts b/x-pack/plugins/actions/server/lib/validate_with_schema.ts index 021c460f4c8151..50231f1c9a3a10 100644 --- a/x-pack/plugins/actions/server/lib/validate_with_schema.ts +++ b/x-pack/plugins/actions/server/lib/validate_with_schema.ts @@ -5,24 +5,44 @@ */ import Boom from 'boom'; -import { ActionType } from '../types'; +import { ActionType, ActionTypeConfig, ActionTypeSecrets, ActionTypeParams } from '../types'; -export function validateParams(actionType: ActionType, value: unknown) { +export function validateParams< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void +>(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'params', value); } -export function validateConfig(actionType: ActionType, value: unknown) { +export function validateConfig< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void +>(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'config', value); } -export function validateSecrets(actionType: ActionType, value: unknown) { +export function validateSecrets< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void +>(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'secrets', value); } type ValidKeys = 'params' | 'config' | 'secrets'; -function validateWithSchema( - actionType: ActionType, +function validateWithSchema< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void +>( + actionType: ActionType, key: ValidKeys, value: unknown ): Record { diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index ac4b332e7fd7a3..ca93e88d012034 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -125,7 +125,9 @@ describe('Actions Plugin', () => { id: 'test', name: 'test', minimumLicenseRequired: 'basic', - async executor() {}, + async executor(options) { + return { status: 'ok', actionId: options.actionId }; + }, }; beforeEach(async () => { diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 5b8b25d02658ba..54d137cc0f617e 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -33,13 +33,20 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../features/serve import { SecurityPluginSetup } from '../../security/server'; import { ActionsConfig } from './config'; -import { Services, ActionType, PreConfiguredAction } from './types'; import { ActionExecutor, TaskRunnerFactory, LicenseState, ILicenseState } from './lib'; import { ActionsClient } from './actions_client'; import { ActionTypeRegistry } from './action_type_registry'; import { createExecutionEnqueuerFunction } from './create_execute_function'; import { registerBuiltInActionTypes } from './builtin_action_types'; import { registerActionsUsageCollector } from './usage'; +import { + Services, + ActionType, + PreConfiguredAction, + ActionTypeConfig, + ActionTypeSecrets, + ActionTypeParams, +} from './types'; import { getActionsConfigurationUtilities } from './actions_config'; @@ -70,7 +77,13 @@ export const EVENT_LOG_ACTIONS = { }; export interface PluginSetupContract { - registerType: (actionType: ActionType) => void; + registerType< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams + >( + actionType: ActionType + ): void; } export interface PluginStartContract { @@ -219,7 +232,13 @@ export class ActionsPlugin implements Plugin, Plugi executeActionRoute(router, this.licenseState); return { - registerType: (actionType: ActionType) => { + registerType: < + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams + >( + actionType: ActionType + ) => { if (!(actionType.minimumLicenseRequired in LICENSE_TYPE)) { throw new Error(`"${actionType.minimumLicenseRequired}" is not a valid license type`); } diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts index 38fca656bef5ac..b668e3460828a8 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/execute.test.ts @@ -71,7 +71,9 @@ describe('executeActionRoute', () => { const router = httpServiceMock.createRouter(); const actionsClient = actionsClientMock.create(); - actionsClient.execute.mockResolvedValueOnce((null as unknown) as ActionTypeExecutorResult); + actionsClient.execute.mockResolvedValueOnce( + (null as unknown) as ActionTypeExecutorResult + ); const [context, req, res] = mockHandlerArguments( { actionsClient }, diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts index 0d49d9a3a256ee..f15a117106210a 100644 --- a/x-pack/plugins/actions/server/routes/execute.ts +++ b/x-pack/plugins/actions/server/routes/execute.ts @@ -48,7 +48,7 @@ export const executeActionRoute = (router: IRouter, licenseState: ILicenseState) const { params } = req.body; const { id } = req.params; try { - const body: ActionTypeExecutorResult = await actionsClient.execute({ + const body: ActionTypeExecutorResult = await actionsClient.execute({ params, actionId: id, }); diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index a8e19e3ff2e79d..ecec45ade04606 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -21,6 +21,9 @@ export type GetServicesFunction = (request: KibanaRequest) => Services; export type ActionTypeRegistryContract = PublicMethodsOf; export type GetBasePathFunction = (spaceId?: string) => string; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; +export type ActionTypeConfig = Record; +export type ActionTypeSecrets = Record; +export type ActionTypeParams = Record; export interface Services { callCluster: ILegacyScopedClusterClient['callAsCurrentUser']; @@ -49,32 +52,27 @@ export interface ActionsConfigType { } // the parameters passed to an action type executor function -export interface ActionTypeExecutorOptions { +export interface ActionTypeExecutorOptions { actionId: string; services: Services; - // This will have to remain `any` until we can extend Action Executors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - config: Record; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - secrets: Record; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - params: Record; + config: Config; + secrets: Secrets; + params: Params; } -export interface ActionResult { +export interface ActionResult { id: string; actionTypeId: string; name: string; - // This will have to remain `any` until we can extend Action Executors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - config?: Record; + config?: Config; isPreconfigured: boolean; } -export interface PreConfiguredAction extends ActionResult { - // This will have to remain `any` until we can extend Action Executors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - secrets: Record; +export interface PreConfiguredAction< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets +> extends ActionResult { + secrets: Secrets; } export interface FindActionResult extends ActionResult { @@ -82,38 +80,45 @@ export interface FindActionResult extends ActionResult { } // the result returned from an action type executor function -export interface ActionTypeExecutorResult { +export interface ActionTypeExecutorResult { actionId: string; status: 'ok' | 'error'; message?: string; serviceMessage?: string; - // This will have to remain `any` until we can extend Action Executors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data?: any; + data?: Data; retry?: null | boolean | Date; } // signature of the action type executor function -export type ExecutorType = ( - options: ActionTypeExecutorOptions -) => Promise; +export type ExecutorType = ( + options: ActionTypeExecutorOptions +) => Promise>; + +interface ValidatorType { + validate(value: unknown): Type; +} -interface ValidatorType { - validate(value: unknown): Record; +export interface ActionValidationService { + isWhitelistedHostname(hostname: string): boolean; + isWhitelistedUri(uri: string): boolean; } -export type ActionTypeCreator = (config?: ActionsConfigType) => ActionType; -export interface ActionType { +export interface ActionType< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void +> { id: string; name: string; maxAttempts?: number; minimumLicenseRequired: LicenseType; validate?: { - params?: ValidatorType; - config?: ValidatorType; - secrets?: ValidatorType; + params?: ValidatorType; + config?: ValidatorType; + secrets?: ValidatorType; }; - executor: ExecutorType; + executor: ExecutorType; } export interface RawAction extends SavedObjectAttributes { diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts index 4f7ef290370cc2..35422fa3c5fef3 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/api.ts @@ -241,16 +241,15 @@ export const pushToService = async ( casePushParams: ServiceConnectorCaseParams, signal: AbortSignal ): Promise => { - const response = await KibanaServices.get().http.fetch( - `${ACTION_URL}/action/${connectorId}/_execute`, - { - method: 'POST', - body: JSON.stringify({ - params: { subAction: 'pushToService', subActionParams: casePushParams }, - }), - signal, - } - ); + const response = await KibanaServices.get().http.fetch< + ActionTypeExecutorResult> + >(`${ACTION_URL}/action/${connectorId}/_execute`, { + method: 'POST', + body: JSON.stringify({ + params: { subAction: 'pushToService', subActionParams: casePushParams }, + }), + signal, + }); if (response.status === 'error') { throw new Error(response.serviceMessage ?? response.message ?? i18n.ERROR_PUSH_TO_SERVICE); diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts index fd0d03dc184102..7c43ac0bbe56f9 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts @@ -5,16 +5,14 @@ */ import { CoreSetup } from 'src/core/server'; -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { FixtureStartDeps, FixtureSetupDeps } from './plugin'; -import { ActionType, ActionTypeExecutorOptions } from '../../../../../../../plugins/actions/server'; +import { ActionType } from '../../../../../../../plugins/actions/server'; export function defineActionTypes( core: CoreSetup, { actions }: Pick ) { - const clusterClient = core.elasticsearch.legacy.client; - // Action types const noopActionType: ActionType = { id: 'test.noop', @@ -32,24 +30,39 @@ export function defineActionTypes( throw new Error('this action is intended to fail'); }, }; - const indexRecordActionType: ActionType = { + actions.registerType(noopActionType); + actions.registerType(throwActionType); + actions.registerType(getIndexRecordActionType()); + actions.registerType(getFailingActionType()); + actions.registerType(getRateLimitedActionType()); + actions.registerType(getAuthorizationActionType(core)); +} + +function getIndexRecordActionType() { + const paramsSchema = schema.object({ + index: schema.string(), + reference: schema.string(), + message: schema.string(), + }); + type ParamsType = TypeOf; + const configSchema = schema.object({ + unencrypted: schema.string(), + }); + type ConfigType = TypeOf; + const secretsSchema = schema.object({ + encrypted: schema.string(), + }); + type SecretsType = TypeOf; + const result: ActionType = { id: 'test.index-record', name: 'Test: Index Record', minimumLicenseRequired: 'gold', validate: { - params: schema.object({ - index: schema.string(), - reference: schema.string(), - message: schema.string(), - }), - config: schema.object({ - unencrypted: schema.string(), - }), - secrets: schema.object({ - encrypted: schema.string(), - }), + params: paramsSchema, + config: configSchema, + secrets: secretsSchema, }, - async executor({ config, secrets, params, services, actionId }: ActionTypeExecutorOptions) { + async executor({ config, secrets, params, services, actionId }) { await services.callCluster('index', { index: params.index, refresh: 'wait_for', @@ -64,17 +77,23 @@ export function defineActionTypes( return { status: 'ok', actionId }; }, }; - const failingActionType: ActionType = { + return result; +} + +function getFailingActionType() { + const paramsSchema = schema.object({ + index: schema.string(), + reference: schema.string(), + }); + type ParamsType = TypeOf; + const result: ActionType<{}, {}, ParamsType> = { id: 'test.failing', name: 'Test: Failing', minimumLicenseRequired: 'gold', validate: { - params: schema.object({ - index: schema.string(), - reference: schema.string(), - }), + params: paramsSchema, }, - async executor({ config, secrets, params, services }: ActionTypeExecutorOptions) { + async executor({ config, secrets, params, services }) { await services.callCluster('index', { index: params.index, refresh: 'wait_for', @@ -89,19 +108,25 @@ export function defineActionTypes( throw new Error(`expected failure for ${params.index} ${params.reference}`); }, }; - const rateLimitedActionType: ActionType = { + return result; +} + +function getRateLimitedActionType() { + const paramsSchema = schema.object({ + index: schema.string(), + reference: schema.string(), + retryAt: schema.number(), + }); + type ParamsType = TypeOf; + const result: ActionType<{}, {}, ParamsType> = { id: 'test.rate-limit', name: 'Test: Rate Limit', minimumLicenseRequired: 'gold', maxAttempts: 2, validate: { - params: schema.object({ - index: schema.string(), - reference: schema.string(), - retryAt: schema.number(), - }), + params: paramsSchema, }, - async executor({ config, params, services }: ActionTypeExecutorOptions) { + async executor({ config, params, services }) { await services.callCluster('index', { index: params.index, refresh: 'wait_for', @@ -119,20 +144,27 @@ export function defineActionTypes( }; }, }; - const authorizationActionType: ActionType = { + return result; +} + +function getAuthorizationActionType(core: CoreSetup) { + const clusterClient = core.elasticsearch.legacy.client; + const paramsSchema = schema.object({ + callClusterAuthorizationIndex: schema.string(), + savedObjectsClientType: schema.string(), + savedObjectsClientId: schema.string(), + index: schema.string(), + reference: schema.string(), + }); + type ParamsType = TypeOf; + const result: ActionType<{}, {}, ParamsType> = { id: 'test.authorization', name: 'Test: Authorization', minimumLicenseRequired: 'gold', validate: { - params: schema.object({ - callClusterAuthorizationIndex: schema.string(), - savedObjectsClientType: schema.string(), - savedObjectsClientId: schema.string(), - index: schema.string(), - reference: schema.string(), - }), + params: paramsSchema, }, - async executor({ params, services, actionId }: ActionTypeExecutorOptions) { + async executor({ params, services, actionId }) { // Call cluster let callClusterSuccess = false; let callClusterError; @@ -200,10 +232,5 @@ export function defineActionTypes( }; }, }; - actions.registerType(noopActionType); - actions.registerType(throwActionType); - actions.registerType(indexRecordActionType); - actions.registerType(failingActionType); - actions.registerType(rateLimitedActionType); - actions.registerType(authorizationActionType); + return result; } From f506279a5556a0a9f41ce750f2d5cec0aa4d524f Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 4 Aug 2020 14:04:45 -0600 Subject: [PATCH 64/74] [Security Solution][Tech Debt] cleans up ts-ignore issues and some smaller linter issues (#74268) ## Summary * Removes ts-ignore where it is not being used * Replaces ts-ignore with the better alternative which is the ts-expect-error --- .../check_circular_deps/run_check_circular_deps_cli.ts | 2 +- .../security_solution/cypress/support/commands.js | 1 - .../security_solution/cypress/tasks/date_picker.ts | 2 -- .../components/event_details/event_fields_browser.tsx | 2 +- .../public/common/components/events_viewer/index.tsx | 1 - .../common/components/import_data_modal/index.tsx | 1 - .../public/common/components/loader/index.tsx | 2 +- .../common/components/matrix_histogram/index.tsx | 1 - .../components/ml/tables/anomalies_host_table.tsx | 4 ++-- .../components/ml/tables/anomalies_network_table.tsx | 4 ++-- .../jobs_table/filters/jobs_table_filters.tsx | 1 - .../public/common/containers/errors/index.test.tsx | 3 +-- .../public/common/lib/compose/helpers.test.ts | 6 ++---- .../components/alerts_table/actions.test.tsx | 6 ++---- .../components/alerts_table/default_config.tsx | 2 +- .../endpoint_hosts/store/mock_host_result_list.ts | 2 +- .../pages/policy/store/policy_details/reducer.ts | 4 ++-- .../components/embeddables/embedded_map_helpers.tsx | 2 +- .../timelines/components/open_timeline/helpers.ts | 1 - .../components/open_timeline/search_row/index.tsx | 1 - .../row_renderers_browser/row_renderers_browser.tsx | 2 +- .../signals/filter_events_with_list.test.ts | 10 +++++----- .../lib/ip_details/elasticsearch_adapter.test.ts | 2 +- .../timeline/convert_saved_object_to_savedtimeline.ts | 1 - .../lib/timeline/routes/update_timelines_route.ts | 1 - .../uncommon_processes/elasticsearch_adapter.test.ts | 2 +- 26 files changed, 25 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts b/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts index 430e4983882cb9..f9ef5b8fde5b51 100644 --- a/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts +++ b/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts @@ -6,7 +6,7 @@ import { resolve } from 'path'; -// @ts-ignore +// @ts-expect-error import madge from 'madge'; import { createFailError, run } from '@kbn/dev-utils'; diff --git a/x-pack/plugins/security_solution/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js index 789759643e3196..4f382f13bcd5d6 100644 --- a/x-pack/plugins/security_solution/cypress/support/commands.js +++ b/x-pack/plugins/security_solution/cypress/support/commands.js @@ -32,7 +32,6 @@ Cypress.Commands.add('stubSecurityApi', function (dataFileName) { cy.on('window:before:load', (win) => { - // @ts-ignore no null, this is a temp hack see issue above win.fetch = null; }); cy.server(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts b/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts index 809498d25c5d8f..2e1d3379dc2020 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts @@ -38,7 +38,6 @@ export const setTimelineEndDate = (date: string) => { cy.get(DATE_PICKER_ABSOLUTE_INPUT).click({ force: true }); cy.get(DATE_PICKER_ABSOLUTE_INPUT).then(($el) => { - // @ts-ignore if (Cypress.dom.isAttached($el)) { cy.wrap($el).click({ force: true }); } @@ -55,7 +54,6 @@ export const setTimelineStartDate = (date: string) => { cy.get(DATE_PICKER_ABSOLUTE_INPUT).click({ force: true }); cy.get(DATE_PICKER_ABSOLUTE_INPUT).then(($el) => { - // @ts-ignore if (Cypress.dom.isAttached($el)) { cy.wrap($el).click({ force: true }); } diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx index af40d4ff18d5a3..00a4e581320bbf 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx @@ -55,7 +55,7 @@ export const EventFieldsBrowser = React.memo( return (
, column `render` callbacks expect complete BrowserField + // @ts-expect-error items going in match Partial, column `render` callbacks expect complete BrowserField items={items} columns={columns} pagination={false} diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 1563eab6039a61..e4520dab4626a8 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -220,7 +220,6 @@ type PropsFromRedux = ConnectedProps; export const StatefulEventsViewer = connector( React.memo( StatefulEventsViewerComponent, - // eslint-disable-next-line complexity (prevProps, nextProps) => prevProps.id === nextProps.id && deepEqual(prevProps.columns, nextProps.columns) && diff --git a/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx index d5d670b4c03ffe..038c116c9fc8c5 100644 --- a/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx @@ -8,7 +8,6 @@ import { EuiButton, EuiButtonEmpty, EuiCheckbox, - // @ts-ignore no-exported-member EuiFilePicker, EuiModal, EuiModalBody, diff --git a/x-pack/plugins/security_solution/public/common/components/loader/index.tsx b/x-pack/plugins/security_solution/public/common/components/loader/index.tsx index e78f148418588c..cd660ae611610f 100644 --- a/x-pack/plugins/security_solution/public/common/components/loader/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/loader/index.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, - // @ts-ignore + // @ts-expect-error EuiLoadingSpinnerSize, EuiText, } from '@elastic/eui'; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index fa512ad1ed80bf..e93ade7191f52d 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -64,7 +64,6 @@ const HeaderChildrenFlexItem = styled(EuiFlexItem)` margin-left: 24px; `; -// @ts-ignore - the EUI type definitions for Panel do no play nice with styled-components const HistogramPanel = styled(Panel)<{ height?: number }>` display: flex; flex-direction: column; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx index 78cd23e6647c02..9bfae686b1a594 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx @@ -77,9 +77,9 @@ const AnomaliesHostTableComponent: React.FC = ({ /> type is not as specific as EUI's... + // @ts-expect-error the Columns type is not as specific as EUI's... columns={columns} - // @ts-ignore ...which leads to `networks` not "matching" the columns + // @ts-expect-error ...which leads to `networks` not "matching" the columns items={hosts} pagination={pagination} sorting={sorting} diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx index 73fe7b1ea5f6e5..af27d411b990d9 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx @@ -67,9 +67,9 @@ const AnomaliesNetworkTableComponent: React.FC = ({ /> type is not as specific as EUI's... + // @ts-expect-error the Columns type is not as specific as EUI's... columns={columns} - // @ts-ignore ...which leads to `networks` not "matching" the columns + // @ts-expect-error ...which leads to `networks` not "matching" the columns items={networks} pagination={pagination} sorting={sorting} diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx index 8cb35fc6891858..4cfb7f8ad2b5bb 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx @@ -11,7 +11,6 @@ import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem, - // @ts-ignore no-exported-member EuiSearchBar, } from '@elastic/eui'; import { EuiSearchBarQuery } from '../../../../../timelines/components/open_timeline/types'; diff --git a/x-pack/plugins/security_solution/public/common/containers/errors/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/errors/index.test.tsx index e1b192df104d74..1bcbebd12b9bc3 100644 --- a/x-pack/plugins/security_solution/public/common/containers/errors/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/errors/index.test.tsx @@ -14,8 +14,7 @@ import { onError } from 'apollo-link-error'; const mockDispatch = jest.fn(); jest.mock('apollo-link-error'); jest.mock('../../store'); -// @ts-ignore -store.getStore.mockReturnValue({ dispatch: mockDispatch }); +(store.getStore as jest.Mock).mockReturnValue({ dispatch: mockDispatch }); describe('errorLinkHandler', () => { const mockGraphQLErrors: GraphQLError = { diff --git a/x-pack/plugins/security_solution/public/common/lib/compose/helpers.test.ts b/x-pack/plugins/security_solution/public/common/lib/compose/helpers.test.ts index 4a3d734d0a6d45..c34027648c8967 100644 --- a/x-pack/plugins/security_solution/public/common/lib/compose/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/lib/compose/helpers.test.ts @@ -18,10 +18,8 @@ jest.mock('../../containers/errors'); const mockWithClientState = 'mockWithClientState'; const mockHttpLink = { mockHttpLink: 'mockHttpLink' }; -// @ts-ignore -withClientState.mockReturnValue(mockWithClientState); -// @ts-ignore -apolloLinkHttp.createHttpLink.mockImplementation(() => mockHttpLink); +(withClientState as jest.Mock).mockReturnValue(mockWithClientState); +(apolloLinkHttp.createHttpLink as jest.Mock).mockImplementation(() => mockHttpLink); describe('getLinks helper', () => { test('It should return links in correct order', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 16d1a1481bc968..c2b51e29c230d4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -255,8 +255,7 @@ describe('alert actions', () => { nonEcsData: [], updateTimelineIsLoading, }); - // @ts-ignore - const createTimelineArg = createTimeline.mock.calls[0][0]; + const createTimelineArg = (createTimeline as jest.Mock).mock.calls[0][0]; expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimelineArg.timeline.kqlQuery.filterQuery.kuery.kind).toEqual('kuery'); @@ -285,8 +284,7 @@ describe('alert actions', () => { nonEcsData: [], updateTimelineIsLoading, }); - // @ts-ignore - const createTimelineArg = createTimeline.mock.calls[0][0]; + const createTimelineArg = (createTimeline as jest.Mock).mock.calls[0][0]; expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimelineArg.timeline.kqlQuery.filterQueryDraft.kind).toEqual('kuery'); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index f38a9107afca98..be9725dac5ff3c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -95,7 +95,7 @@ export const buildShowBuildingBlockFilter = (showBuildingBlockAlerts: boolean): key: 'signal.rule.building_block_type', value: 'exists', }, - // @ts-ignore TODO: Rework parent typings to support ExistsFilter[] + // @ts-expect-error TODO: Rework parent typings to support ExistsFilter[] exists: { field: 'signal.rule.building_block_type' }, }, ]), diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts index 355c2bb5c19fc8..b69e5c5cd0e65c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts @@ -140,7 +140,7 @@ const hostListApiPathHandlerMocks = ({ // Build a GET route handler for each host details based on the list of Hosts passed on input if (hostsResults) { hostsResults.forEach((host) => { - // @ts-ignore + // @ts-expect-error apiHandlers[`/api/endpoint/metadata/${host.metadata.host.id}`] = () => host; }); } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts index e7aa2c8893f8e5..43a6ad2c585b43 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts @@ -109,14 +109,14 @@ export const policyDetailsReducer: ImmutableReducer { /** * this is not safe because `action.payload.policyConfig` may have excess keys */ - // @ts-ignore + // @ts-expect-error newPolicy[section as keyof UIPolicyConfig] = { ...newPolicy[section as keyof UIPolicyConfig], ...newSettings, diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx index c58e53d07acba4..25928197590ea2 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx @@ -114,7 +114,7 @@ export const createEmbeddable = async ( if (!isErrorEmbeddable(embeddableObject)) { embeddableObject.setRenderTooltipContent(renderTooltipContent); - // @ts-ignore + // @ts-expect-error await embeddableObject.setLayerList(getLayerList(indexPatterns)); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index 27920fa297a484..5b5c2c9eeafc0a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -238,7 +238,6 @@ export const getTimelineStatus = ( return duplicate ? TimelineStatus.active : timeline.status; }; -// eslint-disable-next-line complexity export const defaultTimelineToTimelineModel = ( timeline: TimelineResult, duplicate: boolean, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx index 5b927db3c37a96..69f79fb7aece39 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx @@ -9,7 +9,6 @@ import { EuiFilterButton, EuiFlexGroup, EuiFlexItem, - // @ts-ignore EuiSearchBar, } from '@elastic/eui'; import React, { useMemo } from 'react'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx index d2b0ad788fdb52..7baa7c42fb45ec 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -87,7 +87,7 @@ const RowRenderersBrowserComponent = React.forwardRef( const handleNameClick = useCallback( (item: RowRendererOption) => () => { const newSelection = xor([item], notExcludedRowRenderers); - // @ts-ignore + // @ts-expect-error ref?.current?.setSelection(newSelection); // eslint-disable-line no-unused-expressions }, [notExcludedRowRenderers, ref] diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts index 8c39a254e42615..3334cc17b90502 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts @@ -126,7 +126,7 @@ describe('filterEventsAgainstList', () => { ); expect(res.hits.hits.length).toEqual(2); - // @ts-ignore + // @ts-expect-error const ipVals = res.hits.hits.map((item) => item._source.source.ip); expect(['3.3.3.3', '7.7.7.7']).toEqual(ipVals); }); @@ -188,7 +188,7 @@ describe('filterEventsAgainstList', () => { expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect(res.hits.hits.length).toEqual(6); - // @ts-ignore + // @ts-expect-error const ipVals = res.hits.hits.map((item) => item._source.source.ip); expect(['1.1.1.1', '3.3.3.3', '5.5.5.5', '7.7.7.7', '8.8.8.8', '9.9.9.9']).toEqual(ipVals); }); @@ -247,7 +247,7 @@ describe('filterEventsAgainstList', () => { buildRuleMessage, }); expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); - // @ts-ignore + // @ts-expect-error const ipVals = res.hits.hits.map((item) => item._source.source.ip); expect(res.hits.hits.length).toEqual(7); @@ -324,7 +324,7 @@ describe('filterEventsAgainstList', () => { expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect(res.hits.hits.length).toEqual(8); - // @ts-ignore + // @ts-expect-error const ipVals = res.hits.hits.map((item) => item._source.source.ip); expect([ '1.1.1.1', @@ -386,7 +386,7 @@ describe('filterEventsAgainstList', () => { expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect(res.hits.hits.length).toEqual(9); - // @ts-ignore + // @ts-expect-error const ipVals = res.hits.hits.map((item) => item._source.source.ip); expect([ '1.1.1.1', diff --git a/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.test.ts index 6493a3e05bfc98..6249e60d9a2be8 100644 --- a/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.test.ts @@ -45,7 +45,7 @@ describe('elasticsearch_adapter', () => { describe('#getUsers', () => { test('will format edges correctly', () => { - // @ts-ignore Re-work `DatabaseSearchResponse` types as mock ES Response won't match + // @ts-expect-error Re-work `DatabaseSearchResponse` types as mock ES Response won't match const edges = getUsersEdges(mockUsersData); expect(edges).toEqual(mockFormattedUsersEdges); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts index b54d12d7efce35..f888675b60410f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts @@ -37,7 +37,6 @@ const getTimelineTypeAndStatus = ( status: TimelineStatus | null = TimelineStatus.active ) => { // TODO: Added to support legacy TimelineType.draft, can be removed in 7.10 - // @ts-ignore if (timelineType === 'draft') { return { timelineType: TimelineType.default, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts index a622ee9b157062..07ce9a7336d4d7 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts @@ -34,7 +34,6 @@ export const updateTimelinesRoute = ( tags: ['access:securitySolution'], }, }, - // eslint-disable-next-line complexity async (context, request, response) => { const siemResponse = buildSiemResponse(response); diff --git a/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts index 90839f5ac01c41..2a15f1fe074f81 100644 --- a/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts @@ -131,7 +131,7 @@ describe('elasticsearch_adapter', () => { _id: 'id-9', _score: 0, _source: { - // @ts-ignore ts doesn't like seeing the object written this way, but sometimes this is the data we get! + // @ts-expect-error ts doesn't like seeing the object written this way, but sometimes this is the data we get! 'host.id': ['host-id-9'], 'host.name': ['host-9'], }, From 2eb67130f3259573fe2441a4d0a1526ca5e3297f Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Tue, 4 Aug 2020 21:19:05 +0100 Subject: [PATCH 65/74] [Security Solution] styling for notes' panel (#74274) * styling for notes' pannel * styling * remove additional const * styling --- .../timelines/components/notes/index.tsx | 23 ++++--------------- .../timeline/properties/notes_size.ts | 1 - 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx index 957b37a0bd1c27..7d083735e6c718 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx @@ -9,7 +9,6 @@ import { EuiInMemoryTableProps, EuiModalBody, EuiModalHeader, - EuiPanel, EuiSpacer, } from '@elastic/eui'; import React, { useState } from 'react'; @@ -20,7 +19,6 @@ import { Note } from '../../../common/lib/note'; import { AddNote } from './add_note'; import { columns } from './columns'; import { AssociateNote, GetNewNoteId, NotesCount, search, UpdateNote } from './helpers'; -import { NOTES_PANEL_WIDTH, NOTES_PANEL_HEIGHT } from '../timeline/properties/notes_size'; import { TimelineStatusLiteral, TimelineStatus } from '../../../../common/types/timeline'; interface Props { @@ -32,23 +30,12 @@ interface Props { updateNote: UpdateNote; } -const NotesPanel = styled(EuiPanel)` - height: ${NOTES_PANEL_HEIGHT}px; - width: ${NOTES_PANEL_WIDTH}px; - - & thead { - display: none; - } -`; - -NotesPanel.displayName = 'NotesPanel'; - const InMemoryTable: typeof EuiInMemoryTable & { displayName: string } = styled( EuiInMemoryTable as React.ComponentType> )` - overflow-x: hidden; - overflow-y: auto; - height: 220px; + & thead { + display: none; + } ` as any; // eslint-disable-line @typescript-eslint/no-explicit-any InMemoryTable.displayName = 'InMemoryTable'; @@ -60,7 +47,7 @@ export const Notes = React.memo( const isImmutable = status === TimelineStatus.immutable; return ( - + <> @@ -84,7 +71,7 @@ export const Notes = React.memo( sorting={true} /> - + ); } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/notes_size.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/notes_size.ts index 3a01df8f48a3f8..cc979816f01414 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/notes_size.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/notes_size.ts @@ -5,4 +5,3 @@ */ export const NOTES_PANEL_WIDTH = 1024; -export const NOTES_PANEL_HEIGHT = 750; From c123cfead76a7bfe02d831df920bd9ce54fea9ff Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Tue, 4 Aug 2020 15:15:57 -0600 Subject: [PATCH 66/74] Fix TMS not loaded in legacy maps (#73570) Co-authored-by: Elastic Machine --- .../public/map/service_settings.js | 18 ++++++-------- .../public/map/service_settings.test.js | 24 ++++--------------- .../__tests__/region_map_visualization.js | 8 ++----- src/plugins/tile_map/config.ts | 9 ------- .../coordinate_maps_visualization.js | 8 ++----- src/plugins/tile_map/server/index.ts | 1 - 6 files changed, 16 insertions(+), 52 deletions(-) diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index ae40b2c92d40e2..833304378402a1 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -30,6 +30,7 @@ export class ServiceSettings { constructor(mapConfig, tilemapsConfig) { this._mapConfig = mapConfig; this._tilemapsConfig = tilemapsConfig; + this._hasTmsConfigured = typeof tilemapsConfig.url === 'string' && tilemapsConfig.url !== ''; this._showZoomMessage = true; this._emsClient = new EMSClient({ @@ -53,13 +54,10 @@ export class ServiceSettings { linkify: true, }); - // TMS attribution - const attributionFromConfig = _.escape( - markdownIt.render(this._tilemapsConfig.deprecated.config.options.attribution || '') - ); // TMS Options - this.tmsOptionsFromConfig = _.assign({}, this._tilemapsConfig.deprecated.config.options, { - attribution: attributionFromConfig, + this.tmsOptionsFromConfig = _.assign({}, this._tilemapsConfig.options, { + attribution: _.escape(markdownIt.render(this._tilemapsConfig.options.attribution || '')), + url: this._tilemapsConfig.url, }); } @@ -122,7 +120,7 @@ export class ServiceSettings { */ async getTMSServices() { let allServices = []; - if (this._tilemapsConfig.deprecated.isOverridden) { + if (this._hasTmsConfigured) { //use tilemap.* settings from yml const tmsService = _.cloneDeep(this.tmsOptionsFromConfig); tmsService.id = TMS_IN_YML_ID; @@ -210,14 +208,12 @@ export class ServiceSettings { if (tmsServiceConfig.origin === ORIGIN.EMS) { return this._getAttributesForEMSTMSLayer(isDesaturated, isDarkMode); } else if (tmsServiceConfig.origin === ORIGIN.KIBANA_YML) { - const config = this._tilemapsConfig.deprecated.config; - const attrs = _.pick(config, ['url', 'minzoom', 'maxzoom', 'attribution']); + const attrs = _.pick(this._tilemapsConfig, ['url', 'minzoom', 'maxzoom', 'attribution']); return { ...attrs, ...{ origin: ORIGIN.KIBANA_YML } }; } else { //this is an older config. need to resolve this dynamically. if (tmsServiceConfig.id === TMS_IN_YML_ID) { - const config = this._tilemapsConfig.deprecated.config; - const attrs = _.pick(config, ['url', 'minzoom', 'maxzoom', 'attribution']); + const attrs = _.pick(this._tilemapsConfig, ['url', 'minzoom', 'maxzoom', 'attribution']); return { ...attrs, ...{ origin: ORIGIN.KIBANA_YML } }; } else { //assume ems diff --git a/src/plugins/maps_legacy/public/map/service_settings.test.js b/src/plugins/maps_legacy/public/map/service_settings.test.js index 6e416f7fd5c845..b924c3bb530563 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.test.js +++ b/src/plugins/maps_legacy/public/map/service_settings.test.js @@ -49,11 +49,7 @@ describe('service_settings (FKA tile_map test)', function () { }; const defaultTilemapConfig = { - deprecated: { - config: { - options: {}, - }, - }, + options: {}, }; function makeServiceSettings(mapConfigOptions = {}, tilemapOptions = {}) { @@ -160,13 +156,8 @@ describe('service_settings (FKA tile_map test)', function () { serviceSettings = makeServiceSettings( {}, { - deprecated: { - isOverridden: true, - config: { - url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', - options: { minZoom: 0, maxZoom: 20 }, - }, - }, + url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', + options: { minZoom: 0, maxZoom: 20 }, } ); @@ -251,13 +242,8 @@ describe('service_settings (FKA tile_map test)', function () { includeElasticMapsService: false, }, { - deprecated: { - isOverridden: true, - config: { - url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', - options: { minZoom: 0, maxZoom: 20 }, - }, - }, + url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', + options: { minZoom: 0, maxZoom: 20 }, } ); const tilemapServices = await serviceSettings.getTMSServices(); diff --git a/src/plugins/region_map/public/__tests__/region_map_visualization.js b/src/plugins/region_map/public/__tests__/region_map_visualization.js index 0a2a18c7cef4f4..648193e8e2490d 100644 --- a/src/plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/plugins/region_map/public/__tests__/region_map_visualization.js @@ -111,12 +111,8 @@ describe('RegionMapsVisualizationTests', function () { emsLandingPageUrl: '', }; const tilemapsConfig = { - deprecated: { - config: { - options: { - attribution: '123', - }, - }, + options: { + attribution: '123', }, }; const serviceSettings = new ServiceSettings(mapConfig, tilemapsConfig); diff --git a/src/plugins/tile_map/config.ts b/src/plugins/tile_map/config.ts index 435e52103d156d..e754c842911165 100644 --- a/src/plugins/tile_map/config.ts +++ b/src/plugins/tile_map/config.ts @@ -21,15 +21,6 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const configSchema = schema.object({ url: schema.maybe(schema.string()), - deprecated: schema.any({ - defaultValue: { - config: { - options: { - attribution: '', - }, - }, - }, - }), options: schema.object({ attribution: schema.string({ defaultValue: '' }), minZoom: schema.number({ defaultValue: 0, min: 0 }), diff --git a/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js b/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js index 9ff25ce674d3da..f2830e58e0eead 100644 --- a/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js +++ b/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js @@ -98,12 +98,8 @@ describe('CoordinateMapsVisualizationTest', function () { emsLandingPageUrl: '', }; const tilemapsConfig = { - deprecated: { - config: { - options: { - attribution: '123', - }, - }, + options: { + attribution: '123', }, }; diff --git a/src/plugins/tile_map/server/index.ts b/src/plugins/tile_map/server/index.ts index 3381553fe93644..4bf8c98c99d2ca 100644 --- a/src/plugins/tile_map/server/index.ts +++ b/src/plugins/tile_map/server/index.ts @@ -23,7 +23,6 @@ import { configSchema, ConfigSchema } from '../config'; export const config: PluginConfigDescriptor = { exposeToBrowser: { url: true, - deprecated: true, options: true, }, schema: configSchema, From 372d8817a7d93e0d3c28815136555ac7e2cdf376 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 4 Aug 2020 14:27:44 -0700 Subject: [PATCH 67/74] [Reporting] Shorten asset path to help CLI FS Watcher (#74185) --- src/dev/precommit_hook/casing_check_config.js | 12 ++++++------ .../assets/fonts/noto/LICENSE_OFL.txt | 0 .../assets/fonts/noto/NotoSansCJKtc-Medium.ttf | Bin .../assets/fonts/noto/NotoSansCJKtc-Regular.ttf | Bin .../lib/pdf => common}/assets/fonts/noto/index.js | 0 .../pdf => common}/assets/fonts/roboto/LICENSE.txt | 0 .../assets/fonts/roboto/Roboto-Italic.ttf | Bin .../assets/fonts/roboto/Roboto-Medium.ttf | Bin .../assets/fonts/roboto/Roboto-Regular.ttf | Bin .../lib/pdf => common}/assets/img/logo-grey.png | Bin .../export_types/printable_pdf/lib/pdf/index.js | 2 +- 11 files changed, 7 insertions(+), 7 deletions(-) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/fonts/noto/LICENSE_OFL.txt (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/fonts/noto/NotoSansCJKtc-Medium.ttf (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/fonts/noto/NotoSansCJKtc-Regular.ttf (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/fonts/noto/index.js (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/fonts/roboto/LICENSE.txt (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/fonts/roboto/Roboto-Italic.ttf (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/fonts/roboto/Roboto-Medium.ttf (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/fonts/roboto/Roboto-Regular.ttf (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib/pdf => common}/assets/img/logo-grey.png (100%) diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 404ad671746815..36d0ff8f51d88b 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -177,12 +177,12 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'x-pack/plugins/monitoring/public/icons/health-green.svg', 'x-pack/plugins/monitoring/public/icons/health-red.svg', 'x-pack/plugins/monitoring/public/icons/health-yellow.svg', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf', - 'x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/img/logo-grey.png', + 'x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', + 'x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', + 'x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Italic.ttf', + 'x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Medium.ttf', + 'x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Regular.ttf', + 'x-pack/plugins/reporting/server/export_types/common/assets/img/logo-grey.png', 'x-pack/test/functional/es_archives/monitoring/beats-with-restarted-instance/data.json.gz', 'x-pack/test/functional/es_archives/monitoring/beats-with-restarted-instance/mappings.json', 'x-pack/test/functional/es_archives/monitoring/logstash-pipelines/data.json.gz', diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/LICENSE_OFL.txt similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/LICENSE_OFL.txt rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/LICENSE_OFL.txt diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Medium.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Medium.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Regular.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/NotoSansCJKtc-Regular.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/index.js b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/index.js similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/noto/index.js rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/noto/index.js diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/LICENSE.txt b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/LICENSE.txt similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/LICENSE.txt rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/LICENSE.txt diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Italic.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Italic.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Medium.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Medium.ttf rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Medium.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf b/x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Regular.ttf similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/fonts/roboto/Roboto-Regular.ttf rename to x-pack/plugins/reporting/server/export_types/common/assets/fonts/roboto/Roboto-Regular.ttf diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/img/logo-grey.png b/x-pack/plugins/reporting/server/export_types/common/assets/img/logo-grey.png similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/assets/img/logo-grey.png rename to x-pack/plugins/reporting/server/export_types/common/assets/img/logo-grey.png diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js index f9a9d9d85bfd38..1042fd66abad7f 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js @@ -11,7 +11,7 @@ import Printer from 'pdfmake'; import xRegExp from 'xregexp'; import { i18n } from '@kbn/i18n'; -const assetPath = path.resolve(__dirname, 'assets'); +const assetPath = path.resolve(__dirname, '..', '..', '..', 'common', 'assets'); const tableBorderWidth = 1; From 409779d1ed00b799694174af372b91eb26970959 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 4 Aug 2020 15:34:01 -0700 Subject: [PATCH 68/74] [src/dev/build] build Kibana Platform bundles from source (#73591) Co-authored-by: spalger --- .../serializers/absolute_path_serializer.ts | 7 +++-- .../kbn-optimizer/src/common/bundle_cache.ts | 14 +++++++++ .../src/optimizer/get_plugin_bundles.test.ts | 31 ++++++++++--------- .../src/optimizer/get_plugin_bundles.ts | 12 +++++-- .../src/optimizer/optimizer_config.test.ts | 31 +++++++++++++++---- .../src/optimizer/optimizer_config.ts | 20 ++++++++++-- .../tasks/build_kibana_platform_plugins.ts | 18 +++++------ src/dev/build/tasks/copy_source_task.ts | 2 +- vars/kibanaPipeline.groovy | 1 + 9 files changed, 99 insertions(+), 37 deletions(-) diff --git a/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts b/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts index 884614c8b95516..4008cf852c3a81 100644 --- a/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts +++ b/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts @@ -19,9 +19,12 @@ import { REPO_ROOT } from '../repo_root'; -export function createAbsolutePathSerializer(rootPath: string = REPO_ROOT) { +export function createAbsolutePathSerializer( + rootPath: string = REPO_ROOT, + replacement = '' +) { return { test: (value: any) => typeof value === 'string' && value.startsWith(rootPath), - serialize: (value: string) => value.replace(rootPath, '').replace(/\\/g, '/'), + serialize: (value: string) => value.replace(rootPath, replacement).replace(/\\/g, '/'), }; } diff --git a/packages/kbn-optimizer/src/common/bundle_cache.ts b/packages/kbn-optimizer/src/common/bundle_cache.ts index 7607e270b5b4f7..578108fce51faa 100644 --- a/packages/kbn-optimizer/src/common/bundle_cache.ts +++ b/packages/kbn-optimizer/src/common/bundle_cache.ts @@ -104,4 +104,18 @@ export class BundleCache { public getOptimizerCacheKey() { return this.get().optimizerCacheKey; } + + public clear() { + this.state = undefined; + + if (this.path) { + try { + Fs.unlinkSync(this.path); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + } + } } diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts index a823f66cf767b8..702ad16144e7be 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts @@ -21,7 +21,9 @@ import { createAbsolutePathSerializer } from '@kbn/dev-utils'; import { getPluginBundles } from './get_plugin_bundles'; -expect.addSnapshotSerializer(createAbsolutePathSerializer('/repo')); +expect.addSnapshotSerializer(createAbsolutePathSerializer('/repo', '')); +expect.addSnapshotSerializer(createAbsolutePathSerializer('/output', '')); +expect.addSnapshotSerializer(createAbsolutePathSerializer('/outside/of/repo', '')); it('returns a bundle for core and each plugin', () => { expect( @@ -56,46 +58,47 @@ it('returns a bundle for core and each plugin', () => { manifestPath: '/repo/x-pack/plugins/box/kibana.json', }, ], - '/repo' + '/repo', + '/output' ).map((b) => b.toSpec()) ).toMatchInlineSnapshot(` Array [ Object { "banner": undefined, - "contextDir": /plugins/foo, + "contextDir": /plugins/foo, "id": "foo", - "manifestPath": /plugins/foo/kibana.json, - "outputDir": /plugins/foo/target/public, + "manifestPath": /plugins/foo/kibana.json, + "outputDir": /plugins/foo/target/public, "publicDirNames": Array [ "public", ], - "sourceRoot": , + "sourceRoot": , "type": "plugin", }, Object { "banner": undefined, - "contextDir": "/outside/of/repo/plugins/baz", + "contextDir": /plugins/baz, "id": "baz", - "manifestPath": "/outside/of/repo/plugins/baz/kibana.json", - "outputDir": "/outside/of/repo/plugins/baz/target/public", + "manifestPath": /plugins/baz/kibana.json, + "outputDir": /plugins/baz/target/public, "publicDirNames": Array [ "public", ], - "sourceRoot": , + "sourceRoot": , "type": "plugin", }, Object { "banner": "/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements. * Licensed under the Elastic License; you may not use this file except in compliance with the Elastic License. */ ", - "contextDir": /x-pack/plugins/box, + "contextDir": /x-pack/plugins/box, "id": "box", - "manifestPath": /x-pack/plugins/box/kibana.json, - "outputDir": /x-pack/plugins/box/target/public, + "manifestPath": /x-pack/plugins/box/kibana.json, + "outputDir": /x-pack/plugins/box/target/public, "publicDirNames": Array [ "public", ], - "sourceRoot": , + "sourceRoot": , "type": "plugin", }, ] diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts index 9350b9464242af..d2d19dcd87ccac 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts @@ -23,7 +23,11 @@ import { Bundle } from '../common'; import { KibanaPlatformPlugin } from './kibana_platform_plugins'; -export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: string) { +export function getPluginBundles( + plugins: KibanaPlatformPlugin[], + repoRoot: string, + outputRoot: string +) { const xpackDirSlash = Path.resolve(repoRoot, 'x-pack') + Path.sep; return plugins @@ -36,7 +40,11 @@ export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: stri publicDirNames: ['public', ...p.extraPublicDirs], sourceRoot: repoRoot, contextDir: p.directory, - outputDir: Path.resolve(p.directory, 'target/public'), + outputDir: Path.resolve( + outputRoot, + Path.relative(repoRoot, p.directory), + 'target/public' + ), manifestPath: p.manifestPath, banner: p.directory.startsWith(xpackDirSlash) ? `/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements.\n` + diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index f97646e2bbbd3e..afc2dc8952c87a 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -23,16 +23,20 @@ jest.mock('./get_plugin_bundles.ts'); jest.mock('../common/theme_tags.ts'); jest.mock('./filter_by_id.ts'); -import Path from 'path'; -import Os from 'os'; +jest.mock('os', () => { + const realOs = jest.requireActual('os'); + jest.spyOn(realOs, 'cpus').mockImplementation(() => { + return ['foo'] as any; + }); + return realOs; +}); +import Path from 'path'; import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; -import { OptimizerConfig } from './optimizer_config'; +import { OptimizerConfig, ParsedOptions } from './optimizer_config'; import { parseThemeTags } from '../common'; -jest.spyOn(Os, 'cpus').mockReturnValue(['foo'] as any); - expect.addSnapshotSerializer(createAbsolutePathSerializer()); beforeEach(() => { @@ -118,6 +122,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [ /src/plugins, @@ -145,6 +150,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [ /src/plugins, @@ -172,6 +178,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [ /src/plugins, @@ -201,6 +208,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [ /src/plugins, @@ -227,6 +235,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [ /x/y/z, @@ -253,6 +262,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [], "profileWebpack": false, @@ -276,6 +286,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [], "profileWebpack": false, @@ -299,6 +310,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [], "profileWebpack": false, @@ -323,6 +335,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [], "profileWebpack": false, @@ -347,6 +360,7 @@ describe('OptimizerConfig::parseOptions()', () => { "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, + "outputRoot": , "pluginPaths": Array [], "pluginScanDirs": Array [], "profileWebpack": false, @@ -384,18 +398,22 @@ describe('OptimizerConfig::create()', () => { getPluginBundles.mockReturnValue([Symbol('bundle1'), Symbol('bundle2')]); filterById.mockReturnValue(Symbol('filtered bundles')); - jest.spyOn(OptimizerConfig, 'parseOptions').mockImplementation((): any => ({ + jest.spyOn(OptimizerConfig, 'parseOptions').mockImplementation((): { + [key in keyof ParsedOptions]: any; + } => ({ cache: Symbol('parsed cache'), dist: Symbol('parsed dist'), maxWorkerCount: Symbol('parsed max worker count'), pluginPaths: Symbol('parsed plugin paths'), pluginScanDirs: Symbol('parsed plugin scan dirs'), repoRoot: Symbol('parsed repo root'), + outputRoot: Symbol('parsed output root'), watch: Symbol('parsed watch'), themeTags: Symbol('theme tags'), inspectWorkers: Symbol('parsed inspect workers'), profileWebpack: Symbol('parsed profile webpack'), filters: [], + includeCoreBundle: false, })); }); @@ -474,6 +492,7 @@ describe('OptimizerConfig::create()', () => { Array [ Symbol(new platform plugins), Symbol(parsed repo root), + Symbol(parsed output root), ], ], "instances": Array [ diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index 0e588ab36238b3..45598ff8831b0b 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -55,6 +55,13 @@ function omit(obj: T, keys: K[]): Omit { interface Options { /** absolute path to root of the repo/build */ repoRoot: string; + /** + * absolute path to the root directory where output should be written to. This + * defaults to the repoRoot but can be customized to write output somewhere else. + * + * This is how we write output to the build directory in the Kibana build tasks. + */ + outputRoot?: string; /** enable to run the optimizer in watch mode */ watch?: boolean; /** the maximum number of workers that will be created */ @@ -107,8 +114,9 @@ interface Options { themes?: ThemeTag | '*' | ThemeTag[]; } -interface ParsedOptions { +export interface ParsedOptions { repoRoot: string; + outputRoot: string; watch: boolean; maxWorkerCount: number; profileWebpack: boolean; @@ -139,6 +147,11 @@ export class OptimizerConfig { throw new TypeError('repoRoot must be an absolute path'); } + const outputRoot = options.outputRoot ?? repoRoot; + if (!Path.isAbsolute(outputRoot)) { + throw new TypeError('outputRoot must be an absolute path'); + } + /** * BEWARE: this needs to stay roughly synchronized with * `src/core/server/config/env.ts` which determines which paths @@ -182,6 +195,7 @@ export class OptimizerConfig { watch, dist, repoRoot, + outputRoot, maxWorkerCount, profileWebpack, cache, @@ -206,11 +220,11 @@ export class OptimizerConfig { publicDirNames: ['public', 'public/utils'], sourceRoot: options.repoRoot, contextDir: Path.resolve(options.repoRoot, 'src/core'), - outputDir: Path.resolve(options.repoRoot, 'src/core/target/public'), + outputDir: Path.resolve(options.outputRoot, 'src/core/target/public'), }), ] : []), - ...getPluginBundles(plugins, options.repoRoot), + ...getPluginBundles(plugins, options.repoRoot, options.outputRoot), ]; return new OptimizerConfig( diff --git a/src/dev/build/tasks/build_kibana_platform_plugins.ts b/src/dev/build/tasks/build_kibana_platform_plugins.ts index beb5ad40229df7..48625078e9bd15 100644 --- a/src/dev/build/tasks/build_kibana_platform_plugins.ts +++ b/src/dev/build/tasks/build_kibana_platform_plugins.ts @@ -17,7 +17,7 @@ * under the License. */ -import { CiStatsReporter } from '@kbn/dev-utils'; +import { CiStatsReporter, REPO_ROOT } from '@kbn/dev-utils'; import { runOptimizer, OptimizerConfig, @@ -29,9 +29,10 @@ import { Task } from '../lib'; export const BuildKibanaPlatformPlugins: Task = { description: 'Building distributable versions of Kibana platform plugins', - async run(config, log, build) { - const optimizerConfig = OptimizerConfig.create({ - repoRoot: build.resolvePath(), + async run(_, log, build) { + const config = OptimizerConfig.create({ + repoRoot: REPO_ROOT, + outputRoot: build.resolvePath(), cache: false, oss: build.isOss(), examples: false, @@ -42,11 +43,10 @@ export const BuildKibanaPlatformPlugins: Task = { const reporter = CiStatsReporter.fromEnv(log); - await runOptimizer(optimizerConfig) - .pipe( - reportOptimizerStats(reporter, optimizerConfig, log), - logOptimizerState(log, optimizerConfig) - ) + await runOptimizer(config) + .pipe(reportOptimizerStats(reporter, config, log), logOptimizerState(log, config)) .toPromise(); + + await Promise.all(config.bundles.map((b) => b.cache.clear())); }, }; diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index c8489673b83afb..79279997671e5d 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -30,7 +30,7 @@ export const CopySource: Task = { 'src/**', '!src/**/*.{test,test.mocks,mock}.{js,ts,tsx}', '!src/**/mocks.ts', // special file who imports .mock files - '!src/**/{__tests__,__snapshots__,__mocks__}/**', + '!src/**/{target,__tests__,__snapshots__,__mocks__}/**', '!src/test_utils/**', '!src/fixtures/**', '!src/legacy/core_plugins/console/public/tests/**', diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 5f3e8d1a0660dc..173c5b7e117640 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -170,6 +170,7 @@ def uploadCoverageArtifacts(prefix, pattern) { def withGcsArtifactUpload(workerName, closure) { def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ + '**/target/public/.kbn-optimizer-cache', 'target/kibana-*', 'target/test-metrics/*', 'target/kibana-security-solution/**/*.png', From 36f25e66e85242fb16119915821646744711fff4 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 4 Aug 2020 16:00:56 -0700 Subject: [PATCH 69/74] Fixed Alert details does not update page title and breadcrumb (#74214) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed Alert details does not update page title and breadcrumb * Added alert name to details breadcrumb as a dynamic title * Update x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts Co-authored-by: Mike Côté Co-authored-by: Mike Côté --- .../public/application/home.tsx | 4 ++-- .../public/application/lib/breadcrumb.test.ts | 21 ++++++++++++++----- .../public/application/lib/breadcrumb.ts | 14 +++++++++++-- .../components/alert_details.tsx | 16 +++++++++++++- 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index 15099242b6e17d..eb6b1ada3ba933 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -22,7 +22,7 @@ import { import { i18n } from '@kbn/i18n'; import { Section, routeToConnectors, routeToAlerts } from './constants'; -import { getCurrentBreadcrumb } from './lib/breadcrumb'; +import { getAlertingSectionBreadcrumb } from './lib/breadcrumb'; import { getCurrentDocTitle } from './lib/doc_title'; import { useAppDependencies } from './app_context'; import { hasShowActionsCapability } from './lib/capabilities'; @@ -75,7 +75,7 @@ export const TriggersActionsUIHome: React.FunctionComponent { - setBreadcrumbs([getCurrentBreadcrumb(section || 'home')]); + setBreadcrumbs([getAlertingSectionBreadcrumb(section || 'home')]); chrome.docTitle.change(getCurrentDocTitle(section || 'home')); }, [section, chrome, setBreadcrumbs]); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts index 8ba909beff2a8c..f5578aa5271bed 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts @@ -3,25 +3,25 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { getCurrentBreadcrumb } from './breadcrumb'; +import { getAlertingSectionBreadcrumb, getAlertDetailsBreadcrumb } from './breadcrumb'; import { i18n } from '@kbn/i18n'; import { routeToConnectors, routeToAlerts, routeToHome } from '../constants'; -describe('getCurrentBreadcrumb', () => { +describe('getAlertingSectionBreadcrumb', () => { test('if change calls return proper breadcrumb title ', async () => { - expect(getCurrentBreadcrumb('connectors')).toMatchObject({ + expect(getAlertingSectionBreadcrumb('connectors')).toMatchObject({ text: i18n.translate('xpack.triggersActionsUI.connectors.breadcrumbTitle', { defaultMessage: 'Connectors', }), href: `${routeToConnectors}`, }); - expect(getCurrentBreadcrumb('alerts')).toMatchObject({ + expect(getAlertingSectionBreadcrumb('alerts')).toMatchObject({ text: i18n.translate('xpack.triggersActionsUI.alerts.breadcrumbTitle', { defaultMessage: 'Alerts', }), href: `${routeToAlerts}`, }); - expect(getCurrentBreadcrumb('home')).toMatchObject({ + expect(getAlertingSectionBreadcrumb('home')).toMatchObject({ text: i18n.translate('xpack.triggersActionsUI.home.breadcrumbTitle', { defaultMessage: 'Alerts and Actions', }), @@ -29,3 +29,14 @@ describe('getCurrentBreadcrumb', () => { }); }); }); + +describe('getAlertDetailsBreadcrumb', () => { + test('if select an alert should return proper breadcrumb title with alert name ', async () => { + expect(getAlertDetailsBreadcrumb('testId', 'testName')).toMatchObject({ + text: i18n.translate('xpack.triggersActionsUI.alertDetails.breadcrumbTitle', { + defaultMessage: 'testName', + }), + href: '/alert/testId', + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts index 3735942ff97af9..db624688f9c761 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { routeToHome, routeToConnectors, routeToAlerts } from '../constants'; +import { routeToHome, routeToConnectors, routeToAlerts, routeToAlertDetails } from '../constants'; -export const getCurrentBreadcrumb = (type: string): { text: string; href: string } => { +export const getAlertingSectionBreadcrumb = (type: string): { text: string; href: string } => { // Home and sections switch (type) { case 'connectors': @@ -33,3 +33,13 @@ export const getCurrentBreadcrumb = (type: string): { text: string; href: string }; } }; + +export const getAlertDetailsBreadcrumb = ( + id: string, + name: string +): { text: string; href: string } => { + return { + text: name, + href: `${routeToAlertDetails.replace(':alertId', id)}`, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index b1dd78ff59f343..6ee7915e2be71f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, Fragment } from 'react'; +import React, { useState, Fragment, useEffect } from 'react'; import { keyBy } from 'lodash'; import { useHistory } from 'react-router-dom'; import { @@ -29,6 +29,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useAppDependencies } from '../../../app_context'; import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; +import { getAlertingSectionBreadcrumb, getAlertDetailsBreadcrumb } from '../../../lib/breadcrumb'; +import { getCurrentDocTitle } from '../../../lib/doc_title'; import { Alert, AlertType, ActionType } from '../../../../types'; import { ComponentOpts as BulkOperationsComponentOpts, @@ -69,8 +71,20 @@ export const AlertDetails: React.FunctionComponent = ({ docLinks, charts, dataPlugin, + setBreadcrumbs, + chrome, } = useAppDependencies(); + // Set breadcrumb and page title + useEffect(() => { + setBreadcrumbs([ + getAlertingSectionBreadcrumb('alerts'), + getAlertDetailsBreadcrumb(alert.id, alert.name), + ]); + chrome.docTitle.change(getCurrentDocTitle('alerts')); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const canExecuteActions = hasExecuteActionsCapability(capabilities); const canSaveAlert = hasAllPrivilege(alert, alertType) && From 6a77bec233c81410413e539dba84ddce12072f5c Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 4 Aug 2020 20:06:30 -0400 Subject: [PATCH 70/74] [Security Solution][Exceptions] - Fixes exceptions builder nested deletion issue and adds unit tests (#74250) ### Summary - Updates logic for deleting exception item entries in the builder - found that there was a bug in deleting nested entries - Adds more unit tests --- .../exceptions/add_exception_modal/index.tsx | 4 +- .../exceptions/builder/and_badge.test.tsx | 48 ++ .../exceptions/builder/and_badge.tsx | 52 +++ .../builder/entry_delete_button.test.tsx | 136 ++++++ .../builder/entry_delete_button.tsx | 68 +++ ...ntry_item.test.tsx => entry_item.test.tsx} | 17 +- ...{builder_entry_item.tsx => entry_item.tsx} | 3 - ..._item.test.tsx => exception_item.test.tsx} | 55 +-- ..._exception_item.tsx => exception_item.tsx} | 100 +--- .../exceptions/builder/helpers.test.tsx | 6 +- .../components/exceptions/builder/helpers.tsx | 18 +- .../exceptions/builder/index.test.tsx | 422 +++++++++++++++++ .../components/exceptions/builder/index.tsx | 15 +- ....stories.tsx => logic_buttons.stories.tsx} | 17 +- ...ptions.test.tsx => logic_buttons.test.tsx} | 22 +- ...r_button_options.tsx => logic_buttons.tsx} | 4 +- .../exceptions/builder/reducer.test.ts | 441 ++++++++++++++++++ .../exceptions/edit_exception_modal/index.tsx | 4 +- 18 files changed, 1260 insertions(+), 172 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.tsx rename x-pack/plugins/security_solution/public/common/components/exceptions/builder/{builder_entry_item.test.tsx => entry_item.test.tsx} (97%) rename x-pack/plugins/security_solution/public/common/components/exceptions/builder/{builder_entry_item.tsx => entry_item.tsx} (99%) rename x-pack/plugins/security_solution/public/common/components/exceptions/builder/{builder_exception_item.test.tsx => exception_item.test.tsx} (84%) rename x-pack/plugins/security_solution/public/common/components/exceptions/builder/{builder_exception_item.tsx => exception_item.tsx} (57%) create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx rename x-pack/plugins/security_solution/public/common/components/exceptions/builder/{builder_button_options.stories.tsx => logic_buttons.stories.tsx} (90%) rename x-pack/plugins/security_solution/public/common/components/exceptions/builder/{builder_button_options.test.tsx => logic_buttons.test.tsx} (94%) rename x-pack/plugins/security_solution/public/common/components/exceptions/builder/{builder_button_options.tsx => logic_buttons.tsx} (94%) create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index e6eaa4947e4040..7526c52d16fde7 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -31,7 +31,7 @@ import * as i18n from './translations'; import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; -import { ExceptionBuilder } from '../builder'; +import { ExceptionBuilderComponent } from '../builder'; import { Loader } from '../../loader'; import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; @@ -317,7 +317,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ {i18n.EXCEPTION_BUILDER_INFO} - { + test('it renders exceptionItemEntryFirstRowAndBadge for very first exception item in builder', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeTruthy(); + }); + + test('it renders exceptionItemEntryInvisibleAndBadge if "entriesLength" is 1 or less', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryInvisibleAndBadge"]').exists() + ).toBeTruthy(); + }); + + test('it renders regular "and" badge if exception item is not the first one and includes more than one entry', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionItemEntryAndBadge"]').exists()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.tsx new file mode 100644 index 00000000000000..3ce2f704b36434 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +import { AndOrBadge } from '../../and_or_badge'; + +const MyInvisibleAndBadge = styled(EuiFlexItem)` + visibility: hidden; +`; + +const MyFirstRowContainer = styled(EuiFlexItem)` + padding-top: 20px; +`; + +interface BuilderAndBadgeProps { + entriesLength: number; + exceptionItemIndex: number; +} + +export const BuilderAndBadgeComponent = React.memo( + ({ entriesLength, exceptionItemIndex }) => { + const badge = ; + + if (entriesLength > 1 && exceptionItemIndex === 0) { + return ( + + {badge} + + ); + } else if (entriesLength <= 1) { + return ( + + {badge} + + ); + } else { + return ( + + {badge} + + ); + } + } +); + +BuilderAndBadgeComponent.displayName = 'BuilderAndBadge'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.test.tsx new file mode 100644 index 00000000000000..b766a0536d237d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.test.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { mount } from 'enzyme'; +import React from 'react'; + +import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock'; + +import { BuilderEntryDeleteButtonComponent } from './entry_delete_button'; + +describe('BuilderEntryDeleteButtonComponent', () => { + test('it renders firstRowBuilderDeleteButton for very first entry in builder', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="firstRowBuilderDeleteButton"] button')).toHaveLength(1); + }); + + test('it does not render firstRowBuilderDeleteButton if entryIndex is not 0', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="firstRowBuilderDeleteButton"]')).toHaveLength(0); + expect(wrapper.find('[data-test-subj="builderDeleteButton"] button')).toHaveLength(1); + }); + + test('it does not render firstRowBuilderDeleteButton if exceptionItemIndex is not 0', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="firstRowBuilderDeleteButton"]')).toHaveLength(0); + expect(wrapper.find('[data-test-subj="builderDeleteButton"] button')).toHaveLength(1); + }); + + test('it does not render firstRowBuilderDeleteButton if nestedParentIndex is not null', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="firstRowBuilderDeleteButton"]')).toHaveLength(0); + expect(wrapper.find('[data-test-subj="builderDeleteButton"] button')).toHaveLength(1); + }); + + test('it invokes "onDelete" when button is clicked', () => { + const onDelete = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.find('[data-test-subj="builderDeleteButton"] button').simulate('click'); + + expect(onDelete).toHaveBeenCalledTimes(1); + expect(onDelete).toHaveBeenCalledWith(0, null); + }); + + test('it disables button if it is the only entry left and no field has been selected', () => { + const exceptionItem = { + ...getExceptionListItemSchemaMock(), + entries: [{ ...getEntryMatchMock(), field: '' }], + }; + const wrapper = mount( + + ); + + const button = wrapper.find('[data-test-subj="builderDeleteButton"] button').at(0); + + expect(button.prop('disabled')).toBeTruthy(); + }); + + test('it does not disable button if it is the only entry left and field has been selected', () => { + const wrapper = mount( + + ); + + const button = wrapper.find('[data-test-subj="builderDeleteButton"] button').at(0); + + expect(button.prop('disabled')).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.tsx new file mode 100644 index 00000000000000..e63f95064cba01 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_delete_button.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { EuiButtonIcon, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +import { BuilderEntry } from '../types'; + +const MyFirstRowContainer = styled(EuiFlexItem)` + padding-top: 20px; +`; + +interface BuilderEntryDeleteButtonProps { + entries: BuilderEntry[]; + isOnlyItem: boolean; + entryIndex: number; + exceptionItemIndex: number; + nestedParentIndex: number | null; + onDelete: (item: number, parent: number | null) => void; +} + +export const BuilderEntryDeleteButtonComponent = React.memo( + ({ entries, nestedParentIndex, isOnlyItem, entryIndex, exceptionItemIndex, onDelete }) => { + const isDisabled: boolean = + isOnlyItem && + entries.length === 1 && + exceptionItemIndex === 0 && + (entries[0].field == null || entries[0].field === ''); + + const handleDelete = useCallback((): void => { + onDelete(entryIndex, nestedParentIndex); + }, [onDelete, entryIndex, nestedParentIndex]); + + const button = ( + + ); + + if (entryIndex === 0 && exceptionItemIndex === 0 && nestedParentIndex == null) { + // This logic was added to work around it including the field + // labels in centering the delete icon for the first row + return ( + + {button} + + ); + } else { + return ( + + {button} + + ); + } + } +); + +BuilderEntryDeleteButtonComponent.displayName = 'BuilderEntryDeleteButton'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx similarity index 97% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx index 0f54ec29cc5400..2a116c4cd8acf5 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx @@ -8,7 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { BuilderEntryItem } from './builder_entry_item'; +import { BuilderEntryItem } from './entry_item'; import { isOperator, isNotOperator, @@ -64,7 +64,6 @@ describe('BuilderEntryItem', () => { }} showLabel={true} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -91,7 +90,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -122,7 +120,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -155,7 +152,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -188,7 +184,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -221,7 +216,6 @@ describe('BuilderEntryItem', () => { }} showLabel={true} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -254,7 +248,6 @@ describe('BuilderEntryItem', () => { }} showLabel={true} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -287,7 +280,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -323,7 +315,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -377,7 +368,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={jest.fn()} /> ); @@ -416,7 +406,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={mockOnChange} /> ); @@ -451,7 +440,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={mockOnChange} /> ); @@ -486,7 +474,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={mockOnChange} /> ); @@ -521,7 +508,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={mockOnChange} /> ); @@ -556,7 +542,6 @@ describe('BuilderEntryItem', () => { }} showLabel={false} listType="detection" - addNested={false} onChange={mockOnChange} /> ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx index 3883a2fad2cf2c..3044f6d01b745c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx @@ -34,7 +34,6 @@ interface EntryItemProps { indexPattern: IIndexPattern; showLabel: boolean; listType: ExceptionListType; - addNested: boolean; onChange: (arg: BuilderEntry, i: number) => void; onlyShowListOperators?: boolean; } @@ -43,7 +42,6 @@ export const BuilderEntryItem: React.FC = ({ entry, indexPattern, listType, - addNested, showLabel, onChange, onlyShowListOperators = false, @@ -51,7 +49,6 @@ export const BuilderEntryItem: React.FC = ({ const handleFieldChange = useCallback( ([newField]: IFieldType[]): void => { const { updatedEntry, index } = getEntryOnFieldChange(entry, newField); - onChange(updatedEntry, index); }, [onChange, entry] diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx similarity index 84% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx index 7624ce147abd9e..e90639a2c0285f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx @@ -15,11 +15,11 @@ import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/s import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock'; import { getEntryMatchAnyMock } from '../../../../../../lists/common/schemas/types/entry_match_any.mock'; -import { ExceptionListItemComponent } from './builder_exception_item'; +import { BuilderExceptionListItemComponent } from './exception_item'; jest.mock('../../../../common/lib/kibana'); -describe('ExceptionListItemComponent', () => { +describe('BuilderExceptionListItemComponent', () => { const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); beforeAll(() => { @@ -46,7 +46,7 @@ describe('ExceptionListItemComponent', () => { }; const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> - { andLogicIncluded={true} isOnlyItem={false} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> @@ -71,11 +70,11 @@ describe('ExceptionListItemComponent', () => { }); test('it renders "and" badge when more than one exception item entry exists and it is not the first exception item', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock(), getEntryMatchMock()]; const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> - { andLogicIncluded={true} isOnlyItem={false} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> @@ -98,11 +96,11 @@ describe('ExceptionListItemComponent', () => { }); test('it renders indented "and" badge when "andLogicIncluded" is "true" and only one entry exists', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock()]; const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> - { andLogicIncluded={true} isOnlyItem={false} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> @@ -127,11 +124,11 @@ describe('ExceptionListItemComponent', () => { }); test('it renders no "and" badge when "andLogicIncluded" is "false"', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock()]; const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> - { andLogicIncluded={false} isOnlyItem={false} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> @@ -167,7 +163,7 @@ describe('ExceptionListItemComponent', () => { entries: [{ ...getEntryMatchMock(), field: '' }], }; const wrapper = mount( - { andLogicIncluded={false} isOnlyItem={true} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> ); expect( - wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').props().disabled + wrapper.find('[data-test-subj="builderItemEntryDeleteButton"] button').props().disabled ).toBeTruthy(); }); test('it does not render delete button disabled when it is not the only entry left in builder', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock()]; const wrapper = mount( - { andLogicIncluded={false} isOnlyItem={false} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> ); expect( - wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').props().disabled + wrapper.find('[data-test-subj="builderItemEntryDeleteButton"] button').props().disabled ).toBeFalsy(); }); test('it does not render delete button disabled when "exceptionItemIndex" is not "0"', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock()]; const wrapper = mount( - { // this to be true, but done for testing purposes isOnlyItem={true} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> ); expect( - wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').props().disabled + wrapper.find('[data-test-subj="builderItemEntryDeleteButton"] button').props().disabled ).toBeFalsy(); }); test('it does not render delete button disabled when more than one entry exists', () => { - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock(), getEntryMatchMock()]; const wrapper = mount( - { andLogicIncluded={false} isOnlyItem={true} listType="detection" - addNested={false} onDeleteExceptionItem={jest.fn()} onChangeExceptionItem={jest.fn()} /> ); expect( - wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').at(0).props() + wrapper.find('[data-test-subj="builderItemEntryDeleteButton"] button').at(0).props() .disabled ).toBeFalsy(); }); test('it invokes "onChangeExceptionItem" when delete button clicked', () => { const mockOnDeleteExceptionItem = jest.fn(); - const exceptionItem = { ...getExceptionListItemSchemaMock() }; + const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock(), getEntryMatchAnyMock()]; const wrapper = mount( - { andLogicIncluded={false} isOnlyItem={true} listType="detection" - addNested={false} onDeleteExceptionItem={mockOnDeleteExceptionItem} onChangeExceptionItem={jest.fn()} /> ); wrapper - .find('[data-test-subj="exceptionItemEntryDeleteButton"] button') + .find('[data-test-subj="builderItemEntryDeleteButton"] button') .at(0) .simulate('click'); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx similarity index 57% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx index 50a6150833796d..cd8b66acd223a2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx @@ -5,23 +5,16 @@ */ import React, { useMemo, useCallback } from 'react'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common'; -import { AndOrBadge } from '../../and_or_badge'; -import { BuilderEntryItem } from './builder_entry_item'; import { getFormattedBuilderEntries, getUpdatedEntriesOnDelete } from './helpers'; import { FormattedBuilderEntry, ExceptionsBuilderExceptionItem, BuilderEntry } from '../types'; import { ExceptionListType } from '../../../../../public/lists_plugin_deps'; - -const MyInvisibleAndBadge = styled(EuiFlexItem)` - visibility: hidden; -`; - -const MyFirstRowContainer = styled(EuiFlexItem)` - padding-top: 20px; -`; +import { BuilderEntryItem } from './entry_item'; +import { BuilderEntryDeleteButtonComponent } from './entry_delete_button'; +import { BuilderAndBadgeComponent } from './and_badge'; const MyBeautifulLine = styled(EuiFlexItem)` &:after { @@ -33,7 +26,7 @@ const MyBeautifulLine = styled(EuiFlexItem)` } `; -interface ExceptionListItemProps { +interface BuilderExceptionListItemProps { exceptionItem: ExceptionsBuilderExceptionItem; exceptionId: string; exceptionItemIndex: number; @@ -41,13 +34,12 @@ interface ExceptionListItemProps { andLogicIncluded: boolean; isOnlyItem: boolean; listType: ExceptionListType; - addNested: boolean; onDeleteExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; onChangeExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; onlyShowListOperators?: boolean; } -export const ExceptionListItemComponent = React.memo( +export const BuilderExceptionListItemComponent = React.memo( ({ exceptionItem, exceptionId, @@ -55,7 +47,6 @@ export const ExceptionListItemComponent = React.memo( indexPattern, isOnlyItem, listType, - addNested, andLogicIncluded, onDeleteExceptionItem, onChangeExceptionItem, @@ -81,8 +72,8 @@ export const ExceptionListItemComponent = React.memo( (entryIndex: number, parentIndex: number | null): void => { const updatedExceptionItem = getUpdatedEntriesOnDelete( exceptionItem, - parentIndex ? parentIndex : entryIndex, - parentIndex ? entryIndex : null + entryIndex, + parentIndex ); onDeleteExceptionItem(updatedExceptionItem, exceptionItemIndex); @@ -98,63 +89,15 @@ export const ExceptionListItemComponent = React.memo( [exceptionItem.entries, indexPattern] ); - const getAndBadge = useCallback((): JSX.Element => { - const badge = ; - - if (andLogicIncluded && exceptionItem.entries.length > 1 && exceptionItemIndex === 0) { - return ( - - {badge} - - ); - } else if (andLogicIncluded && exceptionItem.entries.length <= 1) { - return ( - - {badge} - - ); - } else if (andLogicIncluded && exceptionItem.entries.length > 1) { - return ( - - {badge} - - ); - } else { - return <>; - } - }, [exceptionItem.entries.length, exceptionItemIndex, andLogicIncluded]); - - const getDeleteButton = useCallback( - (entryIndex: number, parentIndex: number | null): JSX.Element => { - const button = ( - handleDeleteEntry(entryIndex, parentIndex)} - isDisabled={ - isOnlyItem && - exceptionItem.entries.length === 1 && - exceptionItemIndex === 0 && - (exceptionItem.entries[0].field == null || exceptionItem.entries[0].field === '') - } - aria-label="entryDeleteButton" - className="exceptionItemEntryDeleteButton" - data-test-subj="exceptionItemEntryDeleteButton" - /> - ); - if (entryIndex === 0 && exceptionItemIndex === 0 && parentIndex == null) { - return {button}; - } else { - return {button}; - } - }, - [exceptionItemIndex, exceptionItem.entries, handleDeleteEntry, isOnlyItem] - ); - return ( - {getAndBadge()} + {andLogicIncluded && ( + + )} {entries.map((item, index) => ( @@ -166,7 +109,6 @@ export const ExceptionListItemComponent = React.memo( entry={item} indexPattern={indexPattern} listType={listType} - addNested={addNested} showLabel={ exceptionItemIndex === 0 && index === 0 && item.nested !== 'child' } @@ -174,10 +116,14 @@ export const ExceptionListItemComponent = React.memo( onlyShowListOperators={onlyShowListOperators} /> - {getDeleteButton( - item.entryIndex, - item.parent != null ? item.parent.parentIndex : null - )} + ))} @@ -189,4 +135,4 @@ export const ExceptionListItemComponent = React.memo( } ); -ExceptionListItemComponent.displayName = 'ExceptionListItem'; +BuilderExceptionListItemComponent.displayName = 'BuilderExceptionListItem'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx index 224c99756eb5c7..a3c5d09a0fb645 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx @@ -654,7 +654,7 @@ describe('Exception builder helpers', () => { expect(output).toEqual(expected); }); - test('it removes entry corresponding to "nestedEntryIndex"', () => { + test('it removes nested entry of "entryIndex" with corresponding parent index', () => { const payloadItem: ExceptionsBuilderExceptionItem = { ...getExceptionListItemSchemaMock(), entries: [ @@ -664,10 +664,10 @@ describe('Exception builder helpers', () => { }, ], }; - const output = getUpdatedEntriesOnDelete(payloadItem, 0, 1); + const output = getUpdatedEntriesOnDelete(payloadItem, 0, 0); const expected: ExceptionsBuilderExceptionItem = { ...getExceptionListItemSchemaMock(), - entries: [{ ...getEntryNestedMock(), entries: [{ ...getEntryExistsMock() }] }], + entries: [{ ...getEntryNestedMock(), entries: [{ ...getEntryMatchAnyMock() }] }], }; expect(output).toEqual(expected); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx index 8585f58504e313..f6b703b7e622ef 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx @@ -248,22 +248,22 @@ export const getFormattedBuilderEntries = ( export const getUpdatedEntriesOnDelete = ( exceptionItem: ExceptionsBuilderExceptionItem, entryIndex: number, - nestedEntryIndex: number | null + nestedParentIndex: number | null ): ExceptionsBuilderExceptionItem => { - const itemOfInterest: BuilderEntry = exceptionItem.entries[entryIndex]; + const itemOfInterest: BuilderEntry = exceptionItem.entries[nestedParentIndex ?? entryIndex]; - if (nestedEntryIndex != null && itemOfInterest.type === OperatorTypeEnum.NESTED) { + if (nestedParentIndex != null && itemOfInterest.type === OperatorTypeEnum.NESTED) { const updatedEntryEntries: Array = [ - ...itemOfInterest.entries.slice(0, nestedEntryIndex), - ...itemOfInterest.entries.slice(nestedEntryIndex + 1), + ...itemOfInterest.entries.slice(0, entryIndex), + ...itemOfInterest.entries.slice(entryIndex + 1), ]; if (updatedEntryEntries.length === 0) { return { ...exceptionItem, entries: [ - ...exceptionItem.entries.slice(0, entryIndex), - ...exceptionItem.entries.slice(entryIndex + 1), + ...exceptionItem.entries.slice(0, nestedParentIndex), + ...exceptionItem.entries.slice(nestedParentIndex + 1), ], }; } else { @@ -277,9 +277,9 @@ export const getUpdatedEntriesOnDelete = ( return { ...exceptionItem, entries: [ - ...exceptionItem.entries.slice(0, entryIndex), + ...exceptionItem.entries.slice(0, nestedParentIndex), updatedItemOfInterest, - ...exceptionItem.entries.slice(entryIndex + 1), + ...exceptionItem.entries.slice(nestedParentIndex + 1), ], }; } diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx new file mode 100644 index 00000000000000..3fa0e59f9acb0c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx @@ -0,0 +1,422 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { wait as waitFor } from '@testing-library/react'; + +import { + fields, + getField, +} from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts'; +import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { getEntryMatchAnyMock } from '../../../../../../lists/common/schemas/types/entry_match_any.mock'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { getEmptyValue } from '../../empty_value'; + +import { ExceptionBuilderComponent } from './'; + +jest.mock('../../../../common/lib/kibana'); + +describe('ExceptionBuilderComponent', () => { + const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); + + beforeEach(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + autocomplete: { + getValueSuggestions: getValueSuggestionsMock, + }, + }, + }, + }); + }); + + afterEach(() => { + getValueSuggestionsMock.mockClear(); + }); + + test('it displays empty entry if no "exceptionListItems" are passed in', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('EuiFlexGroup[data-test-subj="exceptionItemEntryContainer"]')).toHaveLength( + 1 + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('Search'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual('is'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').text()).toEqual( + 'Search field value...' + ); + }); + + test('it displays "exceptionListItems" that are passed in', async () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + expect(wrapper.find('EuiFlexGroup[data-test-subj="exceptionItemEntryContainer"]')).toHaveLength( + 1 + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual( + 'is one of' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"]').text()).toEqual( + 'some ip' + ); + + wrapper.unmount(); + }); + + test('it displays "or", "and" and "add nested button" enabled', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionsAndButton"] button').prop('disabled') + ).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="exceptionsOrButton"] button').prop('disabled') + ).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="exceptionsNestedButton"] button').prop('disabled') + ).toBeFalsy(); + }); + + test('it adds an entry when "and" clicked', async () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('EuiFlexGroup[data-test-subj="exceptionItemEntryContainer"]')).toHaveLength( + 1 + ); + + wrapper.find('[data-test-subj="exceptionsAndButton"] button').simulate('click'); + + await waitFor(() => { + expect( + wrapper.find('EuiFlexGroup[data-test-subj="exceptionItemEntryContainer"]') + ).toHaveLength(2); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').at(0).text()).toEqual( + 'Search' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').at(0).text()).toEqual( + 'is' + ); + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').at(0).text() + ).toEqual('Search field value...'); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').at(1).text()).toEqual( + 'Search' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').at(1).text()).toEqual( + 'is' + ); + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').at(1).text() + ).toEqual('Search field value...'); + }); + }); + + test('it adds an exception item when "or" clicked', async () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('EuiFlexGroup[data-test-subj="exceptionEntriesContainer"]')).toHaveLength( + 1 + ); + + wrapper.find('[data-test-subj="exceptionsOrButton"] button').simulate('click'); + + await waitFor(() => { + expect(wrapper.find('EuiFlexGroup[data-test-subj="exceptionEntriesContainer"]')).toHaveLength( + 2 + ); + + const item1 = wrapper.find('EuiFlexGroup[data-test-subj="exceptionEntriesContainer"]').at(0); + const item2 = wrapper.find('EuiFlexGroup[data-test-subj="exceptionEntriesContainer"]').at(1); + + expect(item1.find('[data-test-subj="exceptionBuilderEntryField"]').at(0).text()).toEqual( + 'Search' + ); + expect(item1.find('[data-test-subj="exceptionBuilderEntryOperator"]').at(0).text()).toEqual( + 'is' + ); + expect(item1.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').at(0).text()).toEqual( + 'Search field value...' + ); + + expect(item2.find('[data-test-subj="exceptionBuilderEntryField"]').at(0).text()).toEqual( + 'Search' + ); + expect(item2.find('[data-test-subj="exceptionBuilderEntryOperator"]').at(0).text()).toEqual( + 'is' + ); + expect(item2.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').at(0).text()).toEqual( + 'Search field value...' + ); + }); + }); + + test('it displays empty entry if user deletes last remaining entry', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual( + 'is one of' + ); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"]').text()).toEqual( + 'some ip' + ); + + wrapper.find('[data-test-subj="firstRowBuilderDeleteButton"] button').simulate('click'); + + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('Search'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual('is'); + expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').text()).toEqual( + 'Search field value...' + ); + + wrapper.unmount(); + }); + + test('it displays "and" badge if at least one exception item includes more than one entry', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeFalsy(); + + wrapper.find('[data-test-subj="exceptionsAndButton"] button').simulate('click'); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeTruthy(); + }); + + test('it does not display "and" badge if none of the exception items include more than one entry', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="exceptionsOrButton"] button').simulate('click'); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeFalsy(); + + wrapper.find('[data-test-subj="exceptionsOrButton"] button').simulate('click'); + + expect( + wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists() + ).toBeFalsy(); + }); + + describe('nested entry', () => { + test('it adds a nested entry when "add nested entry" clicked', async () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="exceptionsNestedButton"] button').simulate('click'); + + await waitFor(() => { + const entry2 = wrapper + .find('EuiFlexGroup[data-test-subj="exceptionItemEntryContainer"]') + .at(1); + expect(entry2.find('[data-test-subj="exceptionBuilderEntryField"]').at(0).text()).toEqual( + 'Search nested field' + ); + expect( + entry2.find('[data-test-subj="exceptionBuilderEntryOperator"]').at(0).text() + ).toEqual('is'); + expect( + entry2.find('[data-test-subj="exceptionBuilderEntryFieldExists"]').at(0).text() + ).toEqual(getEmptyValue()); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx index b82607a541aaac..165f3314c2f154 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useReducer } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; -import { ExceptionListItemComponent } from './builder_exception_item'; +import { BuilderExceptionListItemComponent } from './exception_item'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common'; import { ExceptionListItemSchema, @@ -20,7 +20,7 @@ import { entriesNested, } from '../../../../../public/lists_plugin_deps'; import { AndOrBadge } from '../../and_or_badge'; -import { BuilderButtonOptions } from './builder_button_options'; +import { BuilderLogicButtons } from './logic_buttons'; import { getNewExceptionItem, filterExceptionItems } from '../helpers'; import { ExceptionsBuilderExceptionItem, CreateExceptionListItemBuilderSchema } from '../types'; import { State, exceptionsBuilderReducer } from './reducer'; @@ -72,7 +72,7 @@ interface ExceptionBuilderProps { onChange: (arg: OnChangeProps) => void; } -export const ExceptionBuilder = ({ +export const ExceptionBuilderComponent = ({ exceptionListItems, listType, listId, @@ -310,6 +310,8 @@ export const ExceptionBuilder = ({ onChange({ exceptionItems: filterExceptionItems(exceptions), exceptionsToDelete }); }, [onChange, exceptionsToDelete, exceptions]); + // Defaults builder to never be sans entry, instead + // always falls back to an empty entry if user deletes all useEffect(() => { if ( exceptions.length === 0 || @@ -351,13 +353,12 @@ export const ExceptionBuilder = ({ ))} - )} - ( ({ eui: euiLightVars, darkMode: false })}>{storyFn()} )); -storiesOf('Components|Exceptions|BuilderButtonOptions', module) +storiesOf('Exceptions|BuilderLogicButtons', module) .add('and/or buttons', () => { return ( - { return ( - { return ( - { return ( - { return ( - { return ( - { +describe('BuilderLogicButtons', () => { test('it renders "and" and "or" buttons', () => { const wrapper = mount( - { const onOrClicked = jest.fn(); const wrapper = mount( - { const onAndClicked = jest.fn(); const wrapper = mount( - { const onAddClickWhenNested = jest.fn(); const wrapper = mount( - { test('it disables "and" button if "isAndDisabled" is true', () => { const wrapper = mount( - { test('it disables "or" button if "isOrDisabled" is "true"', () => { const wrapper = mount( - { test('it disables "add nested" button if "isNestedDisabled" is "true"', () => { const wrapper = mount( - { const onNestedClicked = jest.fn(); const wrapper = mount( - { const onAndClicked = jest.fn(); const wrapper = mount( - void; } -export const BuilderButtonOptions: React.FC = ({ +export const BuilderLogicButtons: React.FC = ({ isOrDisabled = false, isAndDisabled = false, showNestedButton = false, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts new file mode 100644 index 00000000000000..ee5bd1329f35bd --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts @@ -0,0 +1,441 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock'; +import { getEntryNestedMock } from '../../../../../../lists/common/schemas/types/entry_nested.mock'; +import { getEntryListMock } from '../../../../../../lists/common/schemas/types/entry_list.mock'; + +import { ExceptionsBuilderExceptionItem } from '../types'; +import { Action, State, exceptionsBuilderReducer } from './reducer'; +import { getDefaultEmptyEntry } from './helpers'; + +const initialState: State = { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: false, + addNested: false, + exceptions: [], + exceptionsToDelete: [], +}; + +describe('exceptionsBuilderReducer', () => { + let reducer: (state: State, action: Action) => State; + + beforeEach(() => { + reducer = exceptionsBuilderReducer(); + }); + + describe('#setExceptions', () => { + test('should return "andLogicIncluded" ', () => { + const update = reducer(initialState, { + type: 'setExceptions', + exceptions: [], + }); + + expect(update).toEqual({ + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: false, + addNested: false, + exceptions: [], + exceptionsToDelete: [], + }); + }); + + test('should set "andLogicIncluded" to true if any of the exceptions include entries with length greater than 1 ', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryMatchMock()], + }, + ]; + const { andLogicIncluded } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(andLogicIncluded).toBeTruthy(); + }); + + test('should set "andLogicIncluded" to false if any of the exceptions include entries with length greater than 1 ', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + ]; + const { andLogicIncluded } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(andLogicIncluded).toBeFalsy(); + }); + + test('should set "addNested" to true if last exception entry is type nested', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + ]; + const { addNested } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(addNested).toBeTruthy(); + }); + + test('should set "addNested" to false if last exception item entry is not type nested', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + ]; + const { addNested } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(addNested).toBeFalsy(); + }); + + test('should set "disableOr" to true if last exception entry is type nested', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + ]; + const { disableOr } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableOr).toBeTruthy(); + }); + + test('should set "disableOr" to false if last exception item entry is not type nested', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + ]; + const { disableOr } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableOr).toBeFalsy(); + }); + + test('should set "disableNested" to true if an exception item includes an entry of type list', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryListMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + ]; + const { disableNested } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableNested).toBeTruthy(); + }); + + test('should set "disableNested" to false if an exception item does not include an entry of type list', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + ]; + const { disableNested } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableNested).toBeFalsy(); + }); + + // What does that even mean?! :) Just checking if a user has selected + // to add a nested entry but has not yet selected the nested field + test('should set "disableAnd" to true if last exception item is a nested entry with no entries itself', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryListMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), { ...getEntryNestedMock(), entries: [] }], + }, + ]; + const { disableAnd } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableAnd).toBeTruthy(); + }); + + test('should set "disableAnd" to false if last exception item is a nested entry with no entries itself', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock(), getEntryNestedMock()], + }, + { + ...getExceptionListItemSchemaMock(), + entries: [getEntryMatchMock()], + }, + ]; + const { disableAnd } = reducer(initialState, { + type: 'setExceptions', + exceptions, + }); + + expect(disableAnd).toBeFalsy(); + }); + }); + + describe('#setDefault', () => { + test('should restore initial state and add default empty entry to item" ', () => { + const update = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setDefault', + initialState, + lastException: { + ...getExceptionListItemSchemaMock(), + entries: [], + }, + } + ); + + expect(update).toEqual({ + ...initialState, + exceptions: [ + { + ...getExceptionListItemSchemaMock(), + entries: [getDefaultEmptyEntry()], + }, + ], + }); + }); + }); + + describe('#setExceptionsToDelete', () => { + test('should add passed in exception item to "exceptionsToDelete"', () => { + const exceptions: ExceptionsBuilderExceptionItem[] = [ + { + ...getExceptionListItemSchemaMock(), + id: '1', + entries: [getEntryListMock()], + }, + { + ...getExceptionListItemSchemaMock(), + id: '2', + entries: [getEntryMatchMock(), { ...getEntryNestedMock(), entries: [] }], + }, + ]; + const { exceptionsToDelete } = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions, + exceptionsToDelete: [], + }, + { + type: 'setExceptionsToDelete', + exceptions: [ + { + ...getExceptionListItemSchemaMock(), + id: '1', + entries: [getEntryListMock()], + }, + ], + } + ); + + expect(exceptionsToDelete).toEqual([ + { + ...getExceptionListItemSchemaMock(), + id: '1', + entries: [getEntryListMock()], + }, + ]); + }); + }); + + describe('#setDisableAnd', () => { + test('should set "disableAnd" to false if "action.shouldDisable" is false', () => { + const { disableAnd } = reducer( + { + disableAnd: true, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setDisableAnd', + shouldDisable: false, + } + ); + + expect(disableAnd).toBeFalsy(); + }); + + test('should set "disableAnd" to true if "action.shouldDisable" is true', () => { + const { disableAnd } = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setDisableAnd', + shouldDisable: true, + } + ); + + expect(disableAnd).toBeTruthy(); + }); + }); + + describe('#setDisableOr', () => { + test('should set "disableOr" to false if "action.shouldDisable" is false', () => { + const { disableOr } = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: true, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setDisableOr', + shouldDisable: false, + } + ); + + expect(disableOr).toBeFalsy(); + }); + + test('should set "disableOr" to true if "action.shouldDisable" is true', () => { + const { disableOr } = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setDisableOr', + shouldDisable: true, + } + ); + + expect(disableOr).toBeTruthy(); + }); + }); + + describe('#setAddNested', () => { + test('should set "addNested" to false if "action.addNested" is false', () => { + const { addNested } = reducer( + { + disableAnd: false, + disableNested: true, + disableOr: false, + andLogicIncluded: true, + addNested: true, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setAddNested', + addNested: false, + } + ); + + expect(addNested).toBeFalsy(); + }); + + test('should set "disableOr" to true if "action.addNested" is true', () => { + const { addNested } = reducer( + { + disableAnd: false, + disableNested: false, + disableOr: false, + andLogicIncluded: true, + addNested: false, + exceptions: [getExceptionListItemSchemaMock()], + exceptionsToDelete: [], + }, + { + type: 'setAddNested', + addNested: true, + } + ); + + expect(addNested).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 6109b85f2da5a8..e1352ac38dc496 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -31,7 +31,7 @@ import { import * as i18n from './translations'; import { useKibana } from '../../../lib/kibana'; import { useAppToasts } from '../../../hooks/use_app_toasts'; -import { ExceptionBuilder } from '../builder'; +import { ExceptionBuilderComponent } from '../builder'; import { useAddOrUpdateException } from '../use_add_exception'; import { AddExceptionComments } from '../add_exception_comments'; import { @@ -232,7 +232,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ {i18n.EXCEPTION_BUILDER_INFO} - Date: Tue, 4 Aug 2020 19:36:34 -0600 Subject: [PATCH 71/74] [Security Solution][Detections] Fixes Severity Override not matching for Elastic Endpoint Security rule (#74317) ## Summary Fixes an issue where the `Severity Override` would not match for the `Elastic Endpoint Security` rule. This is a temporary fix until we can provide more robust comparisons between the user provided `severityMapping.value` (string) and the `severityMapping.field`'s type. ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../signals/__mocks__/es_results.ts | 18 ++++++++ .../build_severity_from_mapping.test.ts | 44 ++++++++++++++++++- .../mappings/build_severity_from_mapping.ts | 29 ++++++++++-- 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 513d6a93d1b5b2..95ec753c21fd84 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -109,6 +109,24 @@ export const sampleDocNoSortId = ( sort: [], }); +export const sampleDocSeverity = ( + severity?: Array | string | number | null +): SignalSourceHit => ({ + _index: 'myFakeSignalIndex', + _type: 'doc', + _score: 100, + _version: 1, + _id: sampleIdGuid, + _source: { + someKey: 'someValue', + '@timestamp': '2020-04-20T21:27:45+0000', + event: { + severity: severity ?? 100, + }, + }, + sort: [], +}); + export const sampleEmptyDocSearchResults = (): SignalSearchResponse => ({ took: 10, timed_out: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts index 80950335934f4d..fb1d51364ab393 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sampleDocNoSortId } from '../__mocks__/es_results'; +import { sampleDocNoSortId, sampleDocSeverity } from '../__mocks__/es_results'; import { buildSeverityFromMapping } from './build_severity_from_mapping'; describe('buildSeverityFromMapping', () => { @@ -12,7 +12,7 @@ describe('buildSeverityFromMapping', () => { jest.clearAllMocks(); }); - test('severity defaults to provided if mapping is incomplete', () => { + test('severity defaults to provided if mapping is undefined', () => { const severity = buildSeverityFromMapping({ doc: sampleDocNoSortId(), severity: 'low', @@ -22,5 +22,45 @@ describe('buildSeverityFromMapping', () => { expect(severity).toEqual({ severity: 'low', severityMeta: {} }); }); + test('severity is overridden to highest matched mapping', () => { + const severity = buildSeverityFromMapping({ + doc: sampleDocSeverity(23), + severity: 'low', + severityMapping: [ + { field: 'event.severity', operator: 'equals', value: '23', severity: 'critical' }, + { field: 'event.severity', operator: 'equals', value: '23', severity: 'low' }, + { field: 'event.severity', operator: 'equals', value: '11', severity: 'critical' }, + { field: 'event.severity', operator: 'equals', value: '23', severity: 'medium' }, + ], + }); + + expect(severity).toEqual({ + severity: 'critical', + severityMeta: { + severityOverrideField: 'event.severity', + }, + }); + }); + + test('severity is overridden when field is event.severity and source value is number', () => { + const severity = buildSeverityFromMapping({ + doc: sampleDocSeverity(23), + severity: 'low', + severityMapping: [ + { field: 'event.severity', operator: 'equals', value: '13', severity: 'low' }, + { field: 'event.severity', operator: 'equals', value: '23', severity: 'medium' }, + { field: 'event.severity', operator: 'equals', value: '33', severity: 'high' }, + { field: 'event.severity', operator: 'equals', value: '43', severity: 'critical' }, + ], + }); + + expect(severity).toEqual({ + severity: 'medium', + severityMeta: { + severityOverrideField: 'event.severity', + }, + }); + }); + // TODO: Enhance... }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts index a3c4f47b491be1..c0a62a2cc887d2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts @@ -24,6 +24,13 @@ interface BuildSeverityFromMappingReturn { severityMeta: Meta; // TODO: Stricter types } +const severitySortMapping = { + low: 0, + medium: 1, + high: 2, + critical: 3, +}; + export const buildSeverityFromMapping = ({ doc, severity, @@ -31,10 +38,24 @@ export const buildSeverityFromMapping = ({ }: BuildSeverityFromMappingProps): BuildSeverityFromMappingReturn => { if (severityMapping != null && severityMapping.length > 0) { let severityMatch: SeverityMappingItem | undefined; - severityMapping.forEach((mapping) => { - // TODO: Expand by verifying fieldType from index via doc._index - const mappedValue = get(mapping.field, doc._source); - if (mapping.value === mappedValue) { + + // Sort the SeverityMapping from low to high, so last match (highest severity) is used + const severityMappingSorted = severityMapping.sort( + (a, b) => severitySortMapping[a.severity] - severitySortMapping[b.severity] + ); + + severityMappingSorted.forEach((mapping) => { + const docValue = get(mapping.field, doc._source); + // TODO: Expand by verifying fieldType from index via doc._index + // Till then, explicit parsing of event.severity (long) to number. If not ECS, this could be + // another datatype, but until we can lookup datatype we must assume number for the Elastic + // Endpoint Security rule to function correctly + let parsedMappingValue: string | number = mapping.value; + if (mapping.field === 'event.severity') { + parsedMappingValue = Math.floor(Number(parsedMappingValue)); + } + + if (parsedMappingValue === docValue) { severityMatch = { ...mapping }; } }); From 7c60b09baae541ee7c4bbfe2a6cf06ffb0df736a Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 5 Aug 2020 00:05:27 -0700 Subject: [PATCH 72/74] skip flaky suite (#74327) --- .../functional/apps/monitoring/setup/metricbeat_migration.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js b/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js index 34ac06450f6843..95bd866d386b1d 100644 --- a/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js +++ b/x-pack/test/functional/apps/monitoring/setup/metricbeat_migration.js @@ -11,7 +11,8 @@ export default function ({ getService, getPageObjects }) { const setupMode = getService('monitoringSetupMode'); const PageObjects = getPageObjects(['common', 'console']); - describe('Setup mode metricbeat migration', function () { + // FLAKY: https://github.com/elastic/kibana/issues/74327 + describe.skip('Setup mode metricbeat migration', function () { describe('setup mode btn', () => { const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); From f97641c84ac24438f8b5d509597ab4d905888123 Mon Sep 17 00:00:00 2001 From: EamonnTP Date: Wed, 5 Aug 2020 09:28:20 +0100 Subject: [PATCH 73/74] [DOCS] Add Observability topic (#73041) * Add observability content * Remove xpack from file name * Updates following review * Review edits Co-authored-by: Elastic Machine --- .../images/observability-overview.png | Bin 0 -> 470892 bytes docs/observability/index.asciidoc | 24 ++++++++++++++++++ docs/user/index.asciidoc | 2 ++ 3 files changed, 26 insertions(+) create mode 100644 docs/observability/images/observability-overview.png create mode 100644 docs/observability/index.asciidoc diff --git a/docs/observability/images/observability-overview.png b/docs/observability/images/observability-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..b7d3d09139a891db9a3bccccf8fcf4869e21bc6e GIT binary patch literal 470892 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%0D5-lJ!>b}CC<&pEP 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

0|nJ0(E7ETveds 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%0Dv-qdX3LWII(vp;f9)ckyr6H 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%0Ax|uScf$5<` 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 M${L^|MU%k)52z)LLjV8( literal 0 HcmV?d00001 diff --git a/docs/observability/index.asciidoc b/docs/observability/index.asciidoc new file mode 100644 index 00000000000000..d63402e8df2fbd --- /dev/null +++ b/docs/observability/index.asciidoc @@ -0,0 +1,24 @@ +[chapter] +[role="xpack"] +[[observability]] += Observability + +Observability enables you to add and monitor your logs, system +metrics, uptime data, and application traces, as a single stack. + +With *Observability*, you have: + +* A central place to add and configure your data sources. +* A variety of charts displaying analytics relating to each data source. +* *View in app* options to drill down and analyze data in the Logs, Metrics, Uptime, and APM apps. +* An alerts chart to keep you informed of any issues that you may need to resolve quickly. + +[role="screenshot"] +image::observability/images/observability-overview.png[Observability Overview in {kib}] + +[float] +== Get started + +{kib} provides step-by-step instructions to help you add and configure your data +sources. The {observability-guide}/index.html[Observability Guide] is a good source for more detailed information +and instructions. diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index 01be8c2e264c5c..abbdbeb68d9cbc 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -27,6 +27,8 @@ include::graph/index.asciidoc[] include::visualize.asciidoc[] +include::{kib-repo-dir}/observability/index.asciidoc[] + include::{kib-repo-dir}/logs/index.asciidoc[] include::{kib-repo-dir}/infrastructure/index.asciidoc[] From c24b17f334fed777f65d34ddae104c80be3ec9c9 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Wed, 5 Aug 2020 10:49:36 +0200 Subject: [PATCH 74/74] [Discover] Inline noWhiteSpace function (#74331) * Inline noWhiteSpace function * Fix TypeScript * Remove unused function file --- .../kibana/common/utils/no_white_space.js | 37 ------------------- .../angular/doc_table/components/table_row.ts | 21 +++++++---- 2 files changed, 14 insertions(+), 44 deletions(-) delete mode 100644 src/legacy/core_plugins/kibana/common/utils/no_white_space.js diff --git a/src/legacy/core_plugins/kibana/common/utils/no_white_space.js b/src/legacy/core_plugins/kibana/common/utils/no_white_space.js deleted file mode 100644 index 580418eb3423f4..00000000000000 --- a/src/legacy/core_plugins/kibana/common/utils/no_white_space.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const TAGS_WITH_WS = />\s+<'); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts index 7b862ec518a040..e7fafde2e68d0d 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts @@ -17,13 +17,10 @@ * under the License. */ -import _ from 'lodash'; +import { find, template } from 'lodash'; import $ from 'jquery'; -// @ts-ignore import rison from 'rison-node'; import '../../doc_viewer'; -// @ts-ignore -import { noWhiteSpace } from '../../../../../../../legacy/core_plugins/kibana/common/utils/no_white_space'; import openRowHtml from './table_row/open.html'; import detailsHtml from './table_row/details.html'; @@ -35,6 +32,16 @@ import truncateByHeightTemplateHtml from '../components/table_row/truncate_by_he import { esFilters } from '../../../../../../data/public'; import { getServices } from '../../../../kibana_services'; +const TAGS_WITH_WS = />\s+<'); +} + // guesstimate at the minimum number of chars wide cells in the table should be const MIN_LINE_LENGTH = 20; @@ -43,8 +50,8 @@ interface LazyScope extends ng.IScope { } export function createTableRowDirective($compile: ng.ICompileService, $httpParamSerializer: any) { - const cellTemplate = _.template(noWhiteSpace(cellTemplateHtml)); - const truncateByHeightTemplate = _.template(noWhiteSpace(truncateByHeightTemplateHtml)); + const cellTemplate = template(noWhiteSpace(cellTemplateHtml)); + const truncateByHeightTemplate = template(noWhiteSpace(truncateByHeightTemplateHtml)); return { restrict: 'A', @@ -169,7 +176,7 @@ export function createTableRowDirective($compile: ng.ICompileService, $httpParam const $cell = $cells.eq(i); if ($cell.data('discover:html') === html) return; - const reuse = _.find($cells.slice(i + 1), function (cell: any) { + const reuse = find($cells.slice(i + 1), function (cell: any) { return $.data(cell, 'discover:html') === html; });