diff --git a/docs/infrastructure/images/infra-sysmon.png b/docs/infrastructure/images/infra-sysmon.png index 5b82d8c9b4e19..dd653bb046f45 100644 Binary files a/docs/infrastructure/images/infra-sysmon.png and b/docs/infrastructure/images/infra-sysmon.png differ diff --git a/docs/infrastructure/images/infra-view-metrics.png b/docs/infrastructure/images/infra-view-metrics.png index 9ad862ec6515d..6001f18d283fe 100644 Binary files a/docs/infrastructure/images/infra-view-metrics.png and b/docs/infrastructure/images/infra-view-metrics.png differ diff --git a/docs/infrastructure/images/metrics-add-data.png b/docs/infrastructure/images/metrics-add-data.png index d9640e0d9f5da..f96c30f0e1848 100644 Binary files a/docs/infrastructure/images/metrics-add-data.png and b/docs/infrastructure/images/metrics-add-data.png differ diff --git a/docs/infrastructure/images/metrics-explorer-screen.png b/docs/infrastructure/images/metrics-explorer-screen.png index 7ccf8891678af..6d56491f7d485 100644 Binary files a/docs/infrastructure/images/metrics-explorer-screen.png and b/docs/infrastructure/images/metrics-explorer-screen.png differ diff --git a/docs/logs/images/log-rate-anomalies.png b/docs/logs/images/log-rate-anomalies.png index ac9ff7c9a5235..74ce8d682e1cc 100644 Binary files a/docs/logs/images/log-rate-anomalies.png and b/docs/logs/images/log-rate-anomalies.png differ diff --git a/docs/logs/images/log-rate-entries.png b/docs/logs/images/log-rate-entries.png index f8a3acc9883e0..efa693a2ac529 100644 Binary files a/docs/logs/images/log-rate-entries.png and b/docs/logs/images/log-rate-entries.png differ diff --git a/docs/logs/images/log-time-filter.png b/docs/logs/images/log-time-filter.png index 863e488e6c6c0..ffba6f972aeb7 100644 Binary files a/docs/logs/images/log-time-filter.png and b/docs/logs/images/log-time-filter.png differ diff --git a/docs/logs/images/logs-add-data.png b/docs/logs/images/logs-add-data.png index 2c4a65590aa1b..176c71466aa38 100644 Binary files a/docs/logs/images/logs-add-data.png and b/docs/logs/images/logs-add-data.png differ diff --git a/docs/logs/images/logs-console.png b/docs/logs/images/logs-console.png index 5feb3d9608974..8e94c31c6862a 100644 Binary files a/docs/logs/images/logs-console.png and b/docs/logs/images/logs-console.png differ diff --git a/docs/logs/using.asciidoc b/docs/logs/using.asciidoc index f191f7d746cf8..d84a9260521c7 100644 --- a/docs/logs/using.asciidoc +++ b/docs/logs/using.asciidoc @@ -8,7 +8,6 @@ You can also view related application traces or uptime information where availab [role="screenshot"] image::logs/images/logs-console.png[Logs Console in Kibana] -// ++ Update this [float] [[logs-search]] diff --git a/src/core/TESTING.md b/src/core/TESTING.md index aac54a4a14680..9abc2bb77d7d1 100644 --- a/src/core/TESTING.md +++ b/src/core/TESTING.md @@ -29,7 +29,6 @@ This document outlines best practices and patterns for testing Kibana Plugins. - [Testing dependencies usages](#testing-dependencies-usages) - [Testing components consuming the dependencies](#testing-components-consuming-the-dependencies) - [Testing optional plugin dependencies](#testing-optional-plugin-dependencies) - - [Plugin Contracts](#plugin-contracts) ## Strategy @@ -1082,7 +1081,3 @@ describe('Plugin', () => { }); }); ``` - -## Plugin Contracts - -_How to test your plugin's exposed API_ diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 395e0da218307..36563ba8cbe45 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -25,7 +25,6 @@ import { migrations } from './migrations'; import { importApi } from './server/routes/api/import'; import { exportApi } from './server/routes/api/export'; import { managementApi } from './server/routes/api/management'; -import * as systemApi from './server/lib/system_api'; import mappings from './mappings.json'; import { getUiSettingDefaults } from './ui_setting_defaults'; import { registerCspCollector } from './server/lib/csp_usage_collector'; @@ -323,7 +322,6 @@ export default function(kibana) { exportApi(server); managementApi(server); registerCspCollector(usageCollection, server); - server.expose('systemApi', systemApi); server.injectUiAppVars('kibana', () => injectVars(server)); }, }); diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/sample_data_view_data_button.test.js.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/sample_data_view_data_button.test.js.snap index e08d802406fff..661d1d33a5283 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/sample_data_view_data_button.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/sample_data_view_data_button.test.js.snap @@ -14,6 +14,7 @@ exports[`should render popover when appLinks is not empty 1`] = ` } closePopover={[Function]} + data-test-subj="launchSampleDataSetecommerce" display="inlineBlock" hasArrow={true} id="sampleDataLinksecommerce" diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/sample_data_view_data_button.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/sample_data_view_data_button.js index e6f5c07c94f9f..cb43c18a8e78b 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/sample_data_view_data_button.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/sample_data_view_data_button.js @@ -112,6 +112,7 @@ export class SampleDataViewDataButton extends React.Component { closePopover={this.closePopover} panelPaddingSize="none" anchorPosition="downCenter" + data-test-subj={`launchSampleDataSet${this.props.id}`} > diff --git a/src/legacy/core_plugins/kibana/server/lib/__tests__/system_api.js b/src/legacy/core_plugins/kibana/server/lib/__tests__/system_api.js deleted file mode 100644 index a63a93f3a70d5..0000000000000 --- a/src/legacy/core_plugins/kibana/server/lib/__tests__/system_api.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { isSystemApiRequest } from '../system_api'; - -describe('system_api', () => { - describe('#isSystemApiRequest', () => { - it('returns true for a system API HTTP request', () => { - const mockHapiRequest = { - headers: { - 'kbn-system-api': true, - }, - }; - expect(isSystemApiRequest(mockHapiRequest)).to.be(true); - }); - - it('returns false for a non-system API HTTP request', () => { - const mockHapiRequest = { - headers: {}, - }; - expect(isSystemApiRequest(mockHapiRequest)).to.be(false); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/server/lib/system_api.js b/src/legacy/core_plugins/kibana/server/lib/system_api.js deleted file mode 100644 index 3e2ab667dd98b..0000000000000 --- a/src/legacy/core_plugins/kibana/server/lib/system_api.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const SYSTEM_API_HEADER_NAME = 'kbn-system-api'; - -/** - * Checks on the *server-side*, if an HTTP request is a system API request - * - * @param request HAPI request object - * @return true if request is a system API request; false, otherwise - * @deprecated Use KibanaRequest#isSystemApi - */ -export function isSystemApiRequest(request) { - return !!request.headers[SYSTEM_API_HEADER_NAME]; -} diff --git a/src/legacy/ui/public/system_api/__tests__/system_api.js b/src/legacy/ui/public/system_api/__tests__/system_api.js index 822edaa08fdd6..816024f13f8b2 100644 --- a/src/legacy/ui/public/system_api/__tests__/system_api.js +++ b/src/legacy/ui/public/system_api/__tests__/system_api.js @@ -31,8 +31,8 @@ describe('system_api', () => { }; const newHeaders = addSystemApiHeader(headers); - expect(newHeaders).to.have.property('kbn-system-api'); - expect(newHeaders['kbn-system-api']).to.be(true); + expect(newHeaders).to.have.property('kbn-system-request'); + expect(newHeaders['kbn-system-request']).to.be(true); expect(newHeaders).to.have.property('kbn-version'); expect(newHeaders['kbn-version']).to.be('4.6.0'); @@ -40,7 +40,16 @@ describe('system_api', () => { }); describe('#isSystemApiRequest', () => { - it('returns true for a system API HTTP request', () => { + it('returns true for a system HTTP request', () => { + const mockRequest = { + headers: { + 'kbn-system-request': true, + }, + }; + expect(isSystemApiRequest(mockRequest)).to.be(true); + }); + + it('returns true for a legacy system API HTTP request', () => { const mockRequest = { headers: { 'kbn-system-api': true, diff --git a/src/plugins/kibana_legacy/public/utils/system_api.ts b/src/plugins/kibana_legacy/public/utils/system_api.ts index 397de4dcc2bb3..49d4a78584737 100644 --- a/src/plugins/kibana_legacy/public/utils/system_api.ts +++ b/src/plugins/kibana_legacy/public/utils/system_api.ts @@ -19,7 +19,8 @@ import { IRequestConfig } from 'angular'; -const SYSTEM_API_HEADER_NAME = 'kbn-system-api'; +const SYSTEM_REQUEST_HEADER_NAME = 'kbn-system-request'; +const LEGACY_SYSTEM_API_HEADER_NAME = 'kbn-system-api'; /** * Adds a custom header designating request as system API @@ -28,7 +29,7 @@ const SYSTEM_API_HEADER_NAME = 'kbn-system-api'; */ export function addSystemApiHeader(originalHeaders: Record) { const systemApiHeaders = { - [SYSTEM_API_HEADER_NAME]: true, + [SYSTEM_REQUEST_HEADER_NAME]: true, }; return { ...originalHeaders, @@ -44,5 +45,7 @@ export function addSystemApiHeader(originalHeaders: Record) { */ export function isSystemApiRequest(request: IRequestConfig) { const { headers } = request; - return headers && !!headers[SYSTEM_API_HEADER_NAME]; + return ( + headers && (!!headers[SYSTEM_REQUEST_HEADER_NAME] || !!headers[LEGACY_SYSTEM_API_HEADER_NAME]) + ); } diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts index 8088b5a0f9da9..8bc528e045566 100644 --- a/test/functional/apps/home/_sample_data.ts +++ b/test/functional/apps/home/_sample_data.ts @@ -84,7 +84,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should launch sample flights data set dashboard', async () => { - await PageObjects.home.launchSampleDataSet('flights'); + await PageObjects.home.launchSampleDashboard('flights'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); const todayYearMonthDay = moment().format('MMM D, YYYY'); @@ -96,7 +96,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should render visualizations', async () => { - await PageObjects.home.launchSampleDataSet('flights'); + await PageObjects.home.launchSampleDashboard('flights'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); log.debug('Checking pie charts rendered'); @@ -115,7 +115,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should launch sample logs data set dashboard', async () => { - await PageObjects.home.launchSampleDataSet('logs'); + await PageObjects.home.launchSampleDashboard('logs'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); const todayYearMonthDay = moment().format('MMM D, YYYY'); @@ -127,7 +127,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should launch sample ecommerce data set dashboard', async () => { - await PageObjects.home.launchSampleDataSet('ecommerce'); + await PageObjects.home.launchSampleDashboard('ecommerce'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); const todayYearMonthDay = moment().format('MMM D, YYYY'); diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index a641fbda023c3..6225b4e3aca62 100644 --- a/test/functional/page_objects/home_page.ts +++ b/test/functional/page_objects/home_page.ts @@ -19,9 +19,12 @@ import { FtrProviderContext } from '../ftr_provider_context'; -export function HomePageProvider({ getService }: FtrProviderContext) { +export function HomePageProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const find = getService('find'); + const PageObjects = getPageObjects(['common']); + let isOss = true; class HomePage { async clickSynopsis(title: string) { @@ -63,6 +66,14 @@ export function HomePageProvider({ getService }: FtrProviderContext) { }); } + async launchSampleDashboard(id: string) { + await this.launchSampleDataSet(id); + isOss = await PageObjects.common.isOss(); + if (!isOss) { + await find.clickByLinkText('Dashboard'); + } + } + async launchSampleDataSet(id: string) { await this.addSampleDataSet(id); await testSubjects.click(`launchSampleDataSet${id}`); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap index a45357121354f..ed09b71f0c31c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap @@ -305,7 +305,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` exports[`ErrorGroupOverview -> List should render with data 1`] = ` .c0 { - font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; + font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; } .c1 { @@ -316,7 +316,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` } .c2 { - font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; + font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; font-size: 16px; max-width: 100%; white-space: nowrap; @@ -325,7 +325,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` } .c3 { - font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; + font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; } { - const tooltipState = getTooltipState(getState()); - - if (!tooltipState) { - return; - } - - const nextTooltipFeatures = tooltipState.features.filter(tooltipFeature => { - if (tooltipFeature.layerId !== layerId) { - // feature from another layer, keep it - return true; - } - - // Keep feature if it is still in layer - return layerFeatures.some(layerFeature => { - return layerFeature.properties[FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id; + let featuresRemoved = false; + const openTooltips = getOpenTooltips(getState()) + .map(tooltipState => { + const nextFeatures = tooltipState.features.filter(tooltipFeature => { + if (tooltipFeature.layerId !== layerId) { + // feature from another layer, keep it + return true; + } + + // Keep feature if it is still in layer + return layerFeatures.some(layerFeature => { + return layerFeature.properties[FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id; + }); + }); + + if (tooltipState.features.length !== nextFeatures.length) { + featuresRemoved = true; + } + + return { ...tooltipState, features: nextFeatures }; + }) + .filter(tooltipState => { + return tooltipState.features.length > 0; }); - }); - - if (tooltipState.features.length === nextTooltipFeatures.length) { - // no features got removed, nothing to update - return; - } - if (nextTooltipFeatures.length === 0) { - // all features removed from tooltip, close tooltip - dispatch(setTooltipState(null)); - } else { - dispatch(setTooltipState({ ...tooltipState, features: nextTooltipFeatures })); + if (featuresRemoved) { + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips, + }); } }; } @@ -412,10 +416,61 @@ export function mapExtentChanged(newMapConstants) { }; } -export function setTooltipState(tooltipState) { +export function closeOnClickTooltip(tooltipId) { + return (dispatch, getState) => { + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips: getOpenTooltips(getState()).filter(({ id }) => { + return tooltipId !== id; + }), + }); + }; +} + +export function openOnClickTooltip(tooltipState) { + return (dispatch, getState) => { + const openTooltips = getOpenTooltips(getState()).filter(({ features, location, isLocked }) => { + return ( + isLocked && + !_.isEqual(location, tooltipState.location) && + !_.isEqual(features, tooltipState.features) + ); + }); + + openTooltips.push({ + ...tooltipState, + isLocked: true, + id: uuid(), + }); + + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips, + }); + }; +} + +export function closeOnHoverTooltip() { + return (dispatch, getState) => { + if (getOpenTooltips(getState()).length) { + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips: [], + }); + } + }; +} + +export function openOnHoverTooltip(tooltipState) { return { - type: 'SET_TOOLTIP_STATE', - tooltipState: tooltipState, + type: SET_OPEN_TOOLTIPS, + openTooltips: [ + { + ...tooltipState, + isLocked: false, + id: uuid(), + }, + ], }; } @@ -826,9 +881,9 @@ export function setJoinsForLayer(layer, joins) { } export function updateDrawState(drawState) { - return async dispatch => { + return dispatch => { if (drawState !== null) { - await dispatch(setTooltipState(null)); //tooltips just get in the way + dispatch({ type: SET_OPEN_TOOLTIPS, openTooltips: [] }); // tooltips just get in the way } dispatch({ type: UPDATE_DRAW_STATE, diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js index 0274f849daf3d..9148fbdfd2d1e 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js @@ -16,7 +16,6 @@ import { setMapInitError, } from '../../../actions/map_actions'; import { - getTooltipState, getLayerList, getMapReady, getGoto, @@ -33,7 +32,6 @@ function mapStateToProps(state = {}) { layerList: getLayerList(state), goto: getGoto(state), inspectorAdapters: getInspectorAdapters(state), - tooltipState: getTooltipState(state), scrollZoom: getScrollZoom(state), disableInteractive: isInteractiveDisabled(state), disableTooltipControl: isTooltipControlDisabled(state), diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap index 7e8feeec01bbd..cffa441d04ff5 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap @@ -1,117 +1,97 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`TooltipControl render tooltipState is not provided should not render tooltip popover when tooltipState is not provided 1`] = `""`; +exports[`TooltipControl render should not render tooltips when there are no open tooltips 1`] = `""`; -exports[`TooltipControl render tooltipState is provided should render tooltip popover with custom tooltip content when renderTooltipContent provided 1`] = ` - +exports[`TooltipControl render should render hover tooltip 1`] = ` + - - Custom tooltip content - - +/> `; -exports[`TooltipControl render tooltipState is provided should render tooltip popover with features tooltip content 1`] = ` - +exports[`TooltipControl render should render locked tooltip 1`] = ` + - - - - + } +/> `; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap new file mode 100644 index 0000000000000..d95a418988ae7 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TooltipPopover render should render tooltip popover 1`] = ` + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="mapTooltip" + isOpen={true} + ownFocus={false} + panelPaddingSize="m" + style={ + Object { + "pointerEvents": "none", + "transform": "translate(NaNpx, 2987px)", + } + } +> + + + + +`; + +exports[`TooltipPopover render should render tooltip popover with custom tooltip content when renderTooltipContent provided 1`] = ` + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="mapTooltip" + isOpen={true} + ownFocus={false} + panelPaddingSize="m" + style={ + Object { + "pointerEvents": "none", + "transform": "translate(NaNpx, 2987px)", + } + } +> + + Custom tooltip content + + +`; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js index 6bc9511c6c580..d3cdbfeca3e57 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js @@ -6,28 +6,41 @@ import { connect } from 'react-redux'; import { TooltipControl } from './tooltip_control'; -import { setTooltipState } from '../../../../actions/map_actions'; +import { + closeOnClickTooltip, + openOnClickTooltip, + closeOnHoverTooltip, + openOnHoverTooltip, +} from '../../../../actions/map_actions'; import { getLayerList, - getTooltipState, + getOpenTooltips, + getHasLockedTooltips, isDrawingFilter, } from '../../../../selectors/map_selectors'; function mapStateToProps(state = {}) { return { layerList: getLayerList(state), - tooltipState: getTooltipState(state), + hasLockedTooltips: getHasLockedTooltips(state), isDrawingFilter: isDrawingFilter(state), + openTooltips: getOpenTooltips(state), }; } function mapDispatchToProps(dispatch) { return { - setTooltipState(tooltipState) { - dispatch(setTooltipState(tooltipState)); + closeOnClickTooltip(tooltipId) { + dispatch(closeOnClickTooltip(tooltipId)); + }, + openOnClickTooltip(tooltipState) { + dispatch(openOnClickTooltip(tooltipState)); + }, + closeOnHoverTooltip() { + dispatch(closeOnHoverTooltip()); }, - clearTooltipState() { - dispatch(setTooltipState(null)); + openOnHoverTooltip(tooltipState) { + dispatch(openOnHoverTooltip(tooltipState)); }, }; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js index cfb92a8677455..329d2b7fd2985 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js @@ -6,16 +6,8 @@ import _ from 'lodash'; import React from 'react'; -import { FEATURE_ID_PROPERTY_NAME, LAT_INDEX, LON_INDEX } from '../../../../../common/constants'; -import { FeaturesTooltip } from '../../features_tooltip/features_tooltip'; -import { EuiPopover, EuiText } from '@elastic/eui'; - -export const TOOLTIP_TYPE = { - HOVER: 'HOVER', - LOCKED: 'LOCKED', -}; - -const noop = () => {}; +import { FEATURE_ID_PROPERTY_NAME, LON_INDEX } from '../../../../../common/constants'; +import { TooltipPopover } from './tooltip_popover'; function justifyAnchorLocation(mbLngLat, targetFeature) { let popupAnchorLocation = [mbLngLat.lng, mbLngLat.lat]; // default popup location to mouse location @@ -35,80 +27,23 @@ function justifyAnchorLocation(mbLngLat, targetFeature) { } export class TooltipControl extends React.Component { - state = { - x: undefined, - y: undefined, - }; - - constructor(props) { - super(props); - this._popoverRef = React.createRef(); - } - - static getDerivedStateFromProps(nextProps, prevState) { - if (nextProps.tooltipState) { - const nextPoint = nextProps.mbMap.project(nextProps.tooltipState.location); - if (nextPoint.x !== prevState.x || nextPoint.y !== prevState.y) { - return { - x: nextPoint.x, - y: nextPoint.y, - }; - } - } - - return null; - } - componentDidMount() { this.props.mbMap.on('mouseout', this._onMouseout); this.props.mbMap.on('mousemove', this._updateHoverTooltipState); - this.props.mbMap.on('move', this._updatePopoverPosition); this.props.mbMap.on('click', this._lockTooltip); } - componentDidUpdate() { - if (this.props.tooltipState && this._popoverRef.current) { - this._popoverRef.current.positionPopoverFluid(); - } - } - componentWillUnmount() { this.props.mbMap.off('mouseout', this._onMouseout); this.props.mbMap.off('mousemove', this._updateHoverTooltipState); - this.props.mbMap.off('move', this._updatePopoverPosition); this.props.mbMap.off('click', this._lockTooltip); } _onMouseout = () => { this._updateHoverTooltipState.cancel(); - if (this.props.tooltipState && this.props.tooltipState.type !== TOOLTIP_TYPE.LOCKED) { - this.props.clearTooltipState(); - } - }; - - _updatePopoverPosition = () => { - if (!this.props.tooltipState) { - return; + if (!this.props.hasLockedTooltips) { + this.props.closeOnHoverTooltip(); } - - const lat = this.props.tooltipState.location[LAT_INDEX]; - const lon = this.props.tooltipState.location[LON_INDEX]; - const bounds = this.props.mbMap.getBounds(); - if ( - lat > bounds.getNorth() || - lat < bounds.getSouth() || - lon < bounds.getWest() || - lon > bounds.getEast() - ) { - this.props.clearTooltipState(); - return; - } - - const nextPoint = this.props.mbMap.project(this.props.tooltipState.location); - this.setState({ - x: nextPoint.x, - y: nextPoint.y, - }); }; _getLayerByMbLayerId(mbLayerId) { @@ -148,7 +83,7 @@ export class TooltipControl extends React.Component { _lockTooltip = e => { if (this.props.isDrawingFilter) { - //ignore click events when in draw mode + // ignore click events when in draw mode return; } @@ -156,7 +91,7 @@ export class TooltipControl extends React.Component { const mbFeatures = this._getFeaturesUnderPointer(e.point); if (!mbFeatures.length) { - this.props.clearTooltipState(); + // No features at click location so there is no tooltip to open return; } @@ -164,42 +99,36 @@ export class TooltipControl extends React.Component { const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeataure); const features = this._getIdsForFeatures(mbFeatures); - this.props.setTooltipState({ - type: TOOLTIP_TYPE.LOCKED, + this.props.openOnClickTooltip({ features: features, location: popupAnchorLocation, }); }; _updateHoverTooltipState = _.debounce(e => { - if (this.props.isDrawingFilter) { - //ignore hover events when in draw mode - return; - } - - if (this.props.tooltipState && this.props.tooltipState.type === TOOLTIP_TYPE.LOCKED) { - //ignore hover events when tooltip is locked + if (this.props.isDrawingFilter || this.props.hasLockedTooltips) { + // ignore hover events when in draw mode or when there are locked tooltips return; } const mbFeatures = this._getFeaturesUnderPointer(e.point); if (!mbFeatures.length) { - this.props.clearTooltipState(); + this.props.closeOnHoverTooltip(); return; } const targetMbFeature = mbFeatures[0]; - if (this.props.tooltipState) { - const firstFeature = this.props.tooltipState.features[0]; + if (this.props.openTooltips[0]) { + const firstFeature = this.props.openTooltips[0].features[0]; if (targetMbFeature.properties[FEATURE_ID_PROPERTY_NAME] === firstFeature.id) { + // ignore hover events when hover tooltip is all ready opened for feature return; } } const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeature); const features = this._getIdsForFeatures(mbFeatures); - this.props.setTooltipState({ - type: TOOLTIP_TYPE.HOVER, + this.props.openOnHoverTooltip({ features: features, location: popupAnchorLocation, }); @@ -240,114 +169,32 @@ export class TooltipControl extends React.Component { return this.props.mbMap.queryRenderedFeatures(mbBbox, { layers: mbLayerIds }); } - // Must load original geometry instead of using geometry from mapbox feature. - // Mapbox feature geometry is from vector tile and is not the same as the original geometry. - _loadFeatureGeometry = ({ layerId, featureId }) => { - const tooltipLayer = this._findLayerById(layerId); - if (!tooltipLayer) { - return null; - } - - const targetFeature = tooltipLayer.getFeatureById(featureId); - if (!targetFeature) { - return null; - } - - return targetFeature.geometry; - }; - - _loadFeatureProperties = async ({ layerId, featureId }) => { - const tooltipLayer = this._findLayerById(layerId); - if (!tooltipLayer) { - return []; - } - - const targetFeature = tooltipLayer.getFeatureById(featureId); - if (!targetFeature) { - return []; - } - return await tooltipLayer.getPropertiesForTooltip(targetFeature.properties); - }; - - _loadPreIndexedShape = async ({ layerId, featureId }) => { - const tooltipLayer = this._findLayerById(layerId); - if (!tooltipLayer) { - return null; - } - - const targetFeature = tooltipLayer.getFeatureById(featureId); - if (!targetFeature) { - return null; - } - - return await tooltipLayer.getSource().getPreIndexedShape(targetFeature.properties); - }; - - _findLayerById = layerId => { - return this.props.layerList.find(layer => { - return layer.getId() === layerId; - }); - }; - - _getLayerName = async layerId => { - const layer = this._findLayerById(layerId); - if (!layer) { + render() { + if (this.props.openTooltips.length === 0) { return null; } - return layer.getDisplayName(); - }; - - _renderTooltipContent = () => { - const publicProps = { - addFilters: this.props.addFilters, - closeTooltip: this.props.clearTooltipState, - features: this.props.tooltipState.features, - isLocked: this.props.tooltipState.type === TOOLTIP_TYPE.LOCKED, - loadFeatureProperties: this._loadFeatureProperties, - loadFeatureGeometry: this._loadFeatureGeometry, - getLayerName: this._getLayerName, - }; - - if (this.props.renderTooltipContent) { - return this.props.renderTooltipContent(publicProps); - } - - return ( - - { + const closeTooltip = isLocked + ? () => { + this.props.closeOnClickTooltip(id); + } + : this.props.closeOnHoverTooltip; + return ( + - - ); - }; - - render() { - if (!this.props.tooltipState) { - return null; - } - - const tooltipAnchor = ( - - ); - return ( - - {this._renderTooltipContent()} - - ); + ); + }); } } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js index b9dc668cfb016..620d7cb9ff756 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js @@ -4,21 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../features_tooltip/features_tooltip', () => ({ - FeaturesTooltip: () => { - return mockFeaturesTooltip; +jest.mock('./tooltip_popover', () => ({ + TooltipPopover: () => { + return mockTooltipPopover; }, })); import sinon from 'sinon'; import React from 'react'; import { mount, shallow } from 'enzyme'; -import { TooltipControl, TOOLTIP_TYPE } from './tooltip_control'; +import { TooltipControl } from './tooltip_control'; // mutable map state let featuresAtLocation; -let mapCenter; -let mockMbMapBounds; const layerId = 'tfi3f'; const mbLayerId = 'tfi3f_circle'; @@ -32,48 +30,16 @@ const mockLayer = { canShowTooltip: () => { return true; }, - getFeatureById: () => { - return { - geometry: { - type: 'Point', - coordinates: [102.0, 0.5], - }, - }; - }, }; const mockMbMapHandlers = {}; const mockMBMap = { - project: lonLatArray => { - const lonDistanceFromCenter = Math.abs(lonLatArray[0] - mapCenter[0]); - const latDistanceFromCenter = Math.abs(lonLatArray[1] - mapCenter[1]); - return { - x: lonDistanceFromCenter * 100, - y: latDistanceFromCenter * 100, - }; - }, on: (eventName, callback) => { mockMbMapHandlers[eventName] = callback; }, off: eventName => { delete mockMbMapHandlers[eventName]; }, - getBounds: () => { - return { - getNorth: () => { - return mockMbMapBounds.north; - }, - getSouth: () => { - return mockMbMapBounds.south; - }, - getWest: () => { - return mockMbMapBounds.west; - }, - getEast: () => { - return mockMbMapBounds.east; - }, - }; - }, getLayer: () => {}, queryRenderedFeatures: () => { return featuresAtLocation; @@ -82,16 +48,21 @@ const mockMBMap = { const defaultProps = { mbMap: mockMBMap, - clearTooltipState: () => {}, - setTooltipState: () => {}, + closeOnClickTooltip: () => {}, + openOnClickTooltip: () => {}, + closeOnHoverTooltip: () => {}, + openOnHoverTooltip: () => {}, layerList: [mockLayer], isDrawingFilter: false, addFilters: () => {}, geoFields: [{}], + openTooltips: [], + hasLockedTooltips: false, }; const hoverTooltipState = { - type: TOOLTIP_TYPE.HOVER, + id: '1', + isLocked: false, location: [-120, 30], features: [ { @@ -103,7 +74,8 @@ const hoverTooltipState = { }; const lockedTooltipState = { - type: TOOLTIP_TYPE.LOCKED, + id: '2', + isLocked: true, location: [-120, 30], features: [ { @@ -117,82 +89,79 @@ const lockedTooltipState = { describe('TooltipControl', () => { beforeEach(() => { featuresAtLocation = []; - mapCenter = [0, 0]; - mockMbMapBounds = { - west: -180, - east: 180, - north: 90, - south: -90, - }; }); describe('render', () => { - describe('tooltipState is not provided', () => { - test('should not render tooltip popover when tooltipState is not provided', () => { - const component = shallow(); + test('should not render tooltips when there are no open tooltips', () => { + const component = shallow(); - expect(component).toMatchSnapshot(); - }); + expect(component).toMatchSnapshot(); }); - describe('tooltipState is provided', () => { - test('should render tooltip popover with features tooltip content', () => { - const component = shallow( - - ); + test('should render hover tooltip', () => { + const component = shallow( + + ); - expect(component).toMatchSnapshot(); - }); + expect(component).toMatchSnapshot(); + }); - test('should render tooltip popover with custom tooltip content when renderTooltipContent provided', () => { - const component = shallow( - { - return Custom tooltip content; - }} - /> - ); - - expect(component).toMatchSnapshot(); - }); + test('should render locked tooltip', () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('should un-register all map callbacks on unmount', () => { + const component = mount(); + + expect(Object.keys(mockMbMapHandlers).length).toBe(3); + + component.unmount(); + expect(Object.keys(mockMbMapHandlers).length).toBe(0); }); }); describe('on mouse out', () => { - const clearTooltipStateStub = sinon.stub(); + const closeOnHoverTooltipStub = sinon.stub(); beforeEach(() => { - clearTooltipStateStub.reset(); + closeOnHoverTooltipStub.reset(); }); test('should clear hover tooltip state', () => { mount( ); mockMbMapHandlers.mouseout(); - sinon.assert.calledOnce(clearTooltipStateStub); + sinon.assert.calledOnce(closeOnHoverTooltipStub); }); test('should not clear locked tooltip state', () => { mount( ); mockMbMapHandlers.mouseout(); - sinon.assert.notCalled(clearTooltipStateStub); + sinon.assert.notCalled(closeOnHoverTooltipStub); }); }); @@ -201,44 +170,44 @@ describe('TooltipControl', () => { point: { x: 0, y: 0 }, lngLat: { lng: 0, lat: 0 }, }; - const setTooltipStateStub = sinon.stub(); - const clearTooltipStateStub = sinon.stub(); + const openOnClickTooltipStub = sinon.stub(); + const closeOnClickTooltipStub = sinon.stub(); beforeEach(() => { - setTooltipStateStub.reset(); - clearTooltipStateStub.reset(); + openOnClickTooltipStub.reset(); + closeOnClickTooltipStub.reset(); }); test('should ignore clicks when map is in drawing mode', () => { mount( ); mockMbMapHandlers.click(mockMapMouseEvent); - sinon.assert.notCalled(clearTooltipStateStub); - sinon.assert.notCalled(setTooltipStateStub); + sinon.assert.notCalled(closeOnClickTooltipStub); + sinon.assert.notCalled(openOnClickTooltipStub); }); - test('should clear tooltip state when there are no features at clicked location', () => { + test('should not open tooltip when there are no features at clicked location', () => { featuresAtLocation = []; mount( ); mockMbMapHandlers.click(mockMapMouseEvent); - sinon.assert.calledOnce(clearTooltipStateStub); - sinon.assert.notCalled(setTooltipStateStub); + sinon.assert.notCalled(closeOnClickTooltipStub); + sinon.assert.notCalled(openOnClickTooltipStub); }); test('should set tooltip state when there are features at clicked location and remove duplicate features', () => { @@ -258,93 +227,18 @@ describe('TooltipControl', () => { mount( ); mockMbMapHandlers.click(mockMapMouseEvent); - sinon.assert.notCalled(clearTooltipStateStub); - sinon.assert.calledWith(setTooltipStateStub, { + sinon.assert.notCalled(closeOnClickTooltipStub); + sinon.assert.calledWith(openOnClickTooltipStub, { features: [{ id: 1, layerId: 'tfi3f' }], location: [100, 30], - type: 'LOCKED', }); }); }); - - describe('on map move', () => { - const clearTooltipStateStub = sinon.stub(); - - beforeEach(() => { - clearTooltipStateStub.reset(); - }); - - test('should safely handle map move when there is no tooltip location', () => { - const component = mount( - - ); - - mockMbMapHandlers.move(); - component.update(); - - sinon.assert.notCalled(clearTooltipStateStub); - }); - - test('should update popover location', () => { - const component = mount( - - ); - - // ensure x and y set from original tooltipState.location - expect(component.state('x')).toBe(12000); - expect(component.state('y')).toBe(3000); - - mapCenter = [25, -15]; - mockMbMapHandlers.move(); - component.update(); - - // ensure x and y updated from new map center with same tooltipState.location - expect(component.state('x')).toBe(14500); - expect(component.state('y')).toBe(4500); - - sinon.assert.notCalled(clearTooltipStateStub); - }); - - test('should clear tooltip state if tooltip location is outside map bounds', () => { - const component = mount( - - ); - - // move map bounds outside of hoverTooltipState.location, which is [-120, 30] - mockMbMapBounds = { - west: -180, - east: -170, - north: 90, - south: 80, - }; - mockMbMapHandlers.move(); - component.update(); - - sinon.assert.calledOnce(clearTooltipStateStub); - }); - }); - - test('should un-register all map callbacks on unmount', () => { - const component = mount(); - - expect(Object.keys(mockMbMapHandlers).length).toBe(4); - - component.unmount(); - expect(Object.keys(mockMbMapHandlers).length).toBe(0); - }); }); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js new file mode 100644 index 0000000000000..867c779bc4dba --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component } from 'react'; +import { LAT_INDEX, LON_INDEX } from '../../../../../common/constants'; +import { FeaturesTooltip } from '../../features_tooltip/features_tooltip'; +import { EuiPopover, EuiText } from '@elastic/eui'; + +const noop = () => {}; + +export class TooltipPopover extends Component { + state = { + x: undefined, + y: undefined, + isVisible: true, + }; + + constructor(props) { + super(props); + this._popoverRef = React.createRef(); + } + + componentDidMount() { + this._updatePopoverPosition(); + this.props.mbMap.on('move', this._updatePopoverPosition); + } + + componentDidUpdate() { + if (this._popoverRef.current) { + this._popoverRef.current.positionPopoverFluid(); + } + } + + componentWillUnmount() { + this.props.mbMap.off('move', this._updatePopoverPosition); + } + + _updatePopoverPosition = () => { + const nextPoint = this.props.mbMap.project(this.props.location); + const lat = this.props.location[LAT_INDEX]; + const lon = this.props.location[LON_INDEX]; + const bounds = this.props.mbMap.getBounds(); + this.setState({ + x: nextPoint.x, + y: nextPoint.y, + isVisible: + lat < bounds.getNorth() && + lat > bounds.getSouth() && + lon > bounds.getWest() && + lon < bounds.getEast(), + }); + }; + + // Must load original geometry instead of using geometry from mapbox feature. + // Mapbox feature geometry is from vector tile and is not the same as the original geometry. + _loadFeatureGeometry = ({ layerId, featureId }) => { + const tooltipLayer = this._findLayerById(layerId); + if (!tooltipLayer) { + return null; + } + + const targetFeature = tooltipLayer.getFeatureById(featureId); + if (!targetFeature) { + return null; + } + + return targetFeature.geometry; + }; + + _loadFeatureProperties = async ({ layerId, featureId }) => { + const tooltipLayer = this._findLayerById(layerId); + if (!tooltipLayer) { + return []; + } + + const targetFeature = tooltipLayer.getFeatureById(featureId); + if (!targetFeature) { + return []; + } + return await tooltipLayer.getPropertiesForTooltip(targetFeature.properties); + }; + + _loadPreIndexedShape = async ({ layerId, featureId }) => { + const tooltipLayer = this._findLayerById(layerId); + if (!tooltipLayer) { + return null; + } + + const targetFeature = tooltipLayer.getFeatureById(featureId); + if (!targetFeature) { + return null; + } + + return await tooltipLayer.getSource().getPreIndexedShape(targetFeature.properties); + }; + + _findLayerById = layerId => { + return this.props.layerList.find(layer => { + return layer.getId() === layerId; + }); + }; + + _getLayerName = async layerId => { + const layer = this._findLayerById(layerId); + if (!layer) { + return null; + } + + return layer.getDisplayName(); + }; + + _renderTooltipContent = () => { + const publicProps = { + addFilters: this.props.addFilters, + closeTooltip: this.props.closeTooltip, + features: this.props.features, + isLocked: this.props.isLocked, + loadFeatureProperties: this._loadFeatureProperties, + loadFeatureGeometry: this._loadFeatureGeometry, + getLayerName: this._getLayerName, + }; + + if (this.props.renderTooltipContent) { + return this.props.renderTooltipContent(publicProps); + } + + return ( + + + + ); + }; + + render() { + if (!this.state.isVisible) { + return null; + } + + const tooltipAnchor = ; + // Although tooltip anchors are not visible, they take up horizontal space. + // This horizontal spacing needs to be accounted for in the translate function, + // otherwise the anchors get increasingly pushed to the right away from the actual location. + const offset = this.props.index * 26; + return ( + + {this._renderTooltipContent()} + + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js new file mode 100644 index 0000000000000..bcef03c205b2b --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../features_tooltip/features_tooltip', () => ({ + FeaturesTooltip: () => { + return mockFeaturesTooltip; + }, +})); + +import sinon from 'sinon'; +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { TooltipPopover } from './tooltip_popover'; + +// mutable map state +let mapCenter; +let mockMbMapBounds; + +const layerId = 'tfi3f'; + +const mockMbMapHandlers = {}; +const mockMBMap = { + project: lonLatArray => { + const lonDistanceFromCenter = Math.abs(lonLatArray[0] - mapCenter[0]); + const latDistanceFromCenter = Math.abs(lonLatArray[1] - mapCenter[1]); + return { + x: lonDistanceFromCenter * 100, + y: latDistanceFromCenter * 100, + }; + }, + on: (eventName, callback) => { + mockMbMapHandlers[eventName] = callback; + }, + off: eventName => { + delete mockMbMapHandlers[eventName]; + }, + getBounds: () => { + return { + getNorth: () => { + return mockMbMapBounds.north; + }, + getSouth: () => { + return mockMbMapBounds.south; + }, + getWest: () => { + return mockMbMapBounds.west; + }, + getEast: () => { + return mockMbMapBounds.east; + }, + }; + }, +}; + +const defaultProps = { + mbMap: mockMBMap, + closeTooltip: () => {}, + layerList: [], + isDrawingFilter: false, + addFilters: () => {}, + geoFields: [{}], + location: [-120, 30], + features: [ + { + id: 1, + layerId: layerId, + geometry: {}, + }, + ], + isLocked: false, +}; + +describe('TooltipPopover', () => { + beforeEach(() => { + mapCenter = [0, 0]; + mockMbMapBounds = { + west: -180, + east: 180, + north: 90, + south: -90, + }; + }); + + describe('render', () => { + test('should render tooltip popover', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + test('should render tooltip popover with custom tooltip content when renderTooltipContent provided', () => { + const component = shallow( + { + return Custom tooltip content; + }} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + test('should un-register all map callbacks on unmount', () => { + const component = mount(); + + expect(Object.keys(mockMbMapHandlers).length).toBe(1); + + component.unmount(); + expect(Object.keys(mockMbMapHandlers).length).toBe(0); + }); + }); + + describe('on map move', () => { + const closeTooltipStub = sinon.stub(); + + beforeEach(() => { + closeTooltipStub.reset(); + }); + + test('should update popover location', () => { + const component = mount(); + + // ensure x and y set from original tooltipState.location + expect(component.state('x')).toBe(12000); + expect(component.state('y')).toBe(3000); + + mapCenter = [25, -15]; + mockMbMapHandlers.move(); + component.update(); + + // ensure x and y updated from new map center with same tooltipState.location + expect(component.state('x')).toBe(14500); + expect(component.state('y')).toBe(4500); + + sinon.assert.notCalled(closeTooltipStub); + }); + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/reducers/map.js b/x-pack/legacy/plugins/maps/public/reducers/map.js index 234584d08a311..7e81fb03dd85b 100644 --- a/x-pack/legacy/plugins/maps/public/reducers/map.js +++ b/x-pack/legacy/plugins/maps/public/reducers/map.js @@ -37,7 +37,7 @@ import { ROLLBACK_TO_TRACKED_LAYER_STATE, REMOVE_TRACKED_LAYER_STATE, UPDATE_SOURCE_DATA_REQUEST, - SET_TOOLTIP_STATE, + SET_OPEN_TOOLTIPS, SET_SCROLL_ZOOM, SET_MAP_INIT_ERROR, UPDATE_DRAW_STATE, @@ -97,7 +97,7 @@ const INITIAL_STATE = { ready: false, mapInitError: null, goto: null, - tooltipState: null, + openTooltips: [], mapState: { zoom: null, // setting this value does not adjust map zoom, read only value used to store current map zoom for persisting between sessions center: null, // setting this value does not adjust map view, read only value used to store current map center for persisting between sessions @@ -138,10 +138,10 @@ export function map(state = INITIAL_STATE, action) { return trackCurrentLayerState(state, action.layerId); case ROLLBACK_TO_TRACKED_LAYER_STATE: return rollbackTrackedLayerState(state, action.layerId); - case SET_TOOLTIP_STATE: + case SET_OPEN_TOOLTIPS: return { ...state, - tooltipState: action.tooltipState, + openTooltips: action.openTooltips, }; case SET_MOUSE_COORDINATES: return { diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js index 4b3d1355e4264..d1048a759beca 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js @@ -42,8 +42,14 @@ function createSourceInstance(sourceDescriptor, inspectorAdapters) { return new Source(sourceDescriptor, inspectorAdapters); } -export const getTooltipState = ({ map }) => { - return map.tooltipState; +export const getOpenTooltips = ({ map }) => { + return map && map.openTooltips ? map.openTooltips : []; +}; + +export const getHasLockedTooltips = state => { + return getOpenTooltips(state).some(({ isLocked }) => { + return isLocked; + }); }; export const getMapReady = ({ map }) => map && map.ready; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js index cc9593d946bd1..1ac391c7f84ae 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js @@ -21,7 +21,7 @@ export const jobs = { jobsWithTimerange(dateFormatTz) { return http({ - url: `${basePath()}/jobs/jobs_with_timerange`, + url: `${basePath()}/jobs/jobs_with_time_range`, method: 'POST', data: { dateFormatTz, diff --git a/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts b/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts index 2487943b5efc0..61f21c316be23 100644 --- a/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts +++ b/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts @@ -6,6 +6,7 @@ import { difference } from 'lodash'; import Boom from 'boom'; +import { IScopedClusterClient } from 'src/core/server'; import { EventManager, CalendarEvent } from './event_manager'; interface BasicCalendar { @@ -23,13 +24,12 @@ export interface FormCalendar extends BasicCalendar { } export class CalendarManager { - private _client: any; + private _client: IScopedClusterClient['callAsCurrentUser']; private _eventManager: any; - constructor(isLegacy: boolean, client: any) { - const actualClient = isLegacy === true ? client : client.ml!.mlClient.callAsCurrentUser; - this._client = actualClient; - this._eventManager = new EventManager(actualClient); + constructor(client: any) { + this._client = client; + this._eventManager = new EventManager(client); } async getCalendar(calendarId: string) { diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/groups.js b/x-pack/legacy/plugins/ml/server/models/job_service/groups.js index 58237b2a8a730..91f82f04a9a0c 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/groups.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/groups.js @@ -7,7 +7,7 @@ import { CalendarManager } from '../calendar'; export function groupsProvider(callWithRequest) { - const calMngr = new CalendarManager(true, callWithRequest); + const calMngr = new CalendarManager(callWithRequest); async function getAllGroups() { const groups = {}; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/index.js b/x-pack/legacy/plugins/ml/server/models/job_service/index.js index 5c0eff3112a53..6f409e70e68b8 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/index.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/index.js @@ -14,14 +14,14 @@ import { topCategoriesProvider, } from './new_job'; -export function jobServiceProvider(callWithRequest, request) { +export function jobServiceProvider(callAsCurrentUser) { return { - ...datafeedsProvider(callWithRequest), - ...jobsProvider(callWithRequest), - ...groupsProvider(callWithRequest), - ...newJobCapsProvider(callWithRequest, request), - ...newJobChartsProvider(callWithRequest, request), - ...categorizationExamplesProvider(callWithRequest, request), - ...topCategoriesProvider(callWithRequest, request), + ...datafeedsProvider(callAsCurrentUser), + ...jobsProvider(callAsCurrentUser), + ...groupsProvider(callAsCurrentUser), + ...newJobCapsProvider(callAsCurrentUser), + ...newJobChartsProvider(callAsCurrentUser), + ...categorizationExamplesProvider(callAsCurrentUser), + ...topCategoriesProvider(callAsCurrentUser), }; } diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js b/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js index e60593c9f0ed5..b4b476c1f926e 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js @@ -22,7 +22,7 @@ export function jobsProvider(callWithRequest) { const { forceDeleteDatafeed, getDatafeedIdsByJobId } = datafeedsProvider(callWithRequest); const { getAuditMessagesSummary } = jobAuditMessagesProvider(callWithRequest); const { getLatestBucketTimestampByJob } = resultsServiceProvider(callWithRequest); - const calMngr = new CalendarManager(true, callWithRequest); + const calMngr = new CalendarManager(callWithRequest); async function forceDeleteJob(jobId) { return callWithRequest('ml.deleteJob', { jobId, force: true }); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts index 3cfb552189062..5827201a63661 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts @@ -5,7 +5,7 @@ */ import { cloneDeep } from 'lodash'; -import { Request } from 'src/legacy/server/kbn_server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { Field, Aggregation, @@ -40,22 +40,27 @@ export function fieldServiceProvider( indexPattern: string, isRollup: boolean, callWithRequest: any, - request: Request + savedObjectsClient: SavedObjectsClientContract ) { - return new FieldsService(indexPattern, isRollup, callWithRequest, request); + return new FieldsService(indexPattern, isRollup, callWithRequest, savedObjectsClient); } class FieldsService { private _indexPattern: string; private _isRollup: boolean; private _callWithRequest: any; - private _request: Request; + private _savedObjectsClient: SavedObjectsClientContract; - constructor(indexPattern: string, isRollup: boolean, callWithRequest: any, request: Request) { + constructor( + indexPattern: string, + isRollup: boolean, + callWithRequest: any, + savedObjectsClient: any + ) { this._indexPattern = indexPattern; this._isRollup = isRollup; this._callWithRequest = callWithRequest; - this._request = request; + this._savedObjectsClient = savedObjectsClient; } private async loadFieldCaps(): Promise { @@ -104,7 +109,7 @@ class FieldsService { const rollupService = await rollupServiceProvider( this._indexPattern, this._callWithRequest, - this._request + this._savedObjectsClient ); const rollupConfigs: RollupJob[] | null = await rollupService.getRollupJobs(); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts index 2c8f8a8f82fb8..f1af7614b4232 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts @@ -18,7 +18,7 @@ import cloudwatchJobCaps from './__mocks__/results/cloudwatch_rollup_job_caps.js describe('job_service - job_caps', () => { let callWithRequestNonRollupMock: jest.Mock; let callWithRequestRollupMock: jest.Mock; - let requestMock: any; + let savedObjectsClientMock: any; beforeEach(() => { callWithRequestNonRollupMock = jest.fn((action: string) => { @@ -37,14 +37,10 @@ describe('job_service - job_caps', () => { } }); - requestMock = { - getSavedObjectsClient: jest.fn(() => { - return { - async find() { - return Promise.resolve(kibanaSavedObjects); - }, - }; - }), + savedObjectsClientMock = { + async find() { + return Promise.resolve(kibanaSavedObjects); + }, }; }); @@ -52,8 +48,8 @@ describe('job_service - job_caps', () => { it('can get job caps for index pattern', async done => { const indexPattern = 'farequote-*'; const isRollup = false; - const { newJobCaps } = newJobCapsProvider(callWithRequestNonRollupMock, requestMock); - const response = await newJobCaps(indexPattern, isRollup); + const { newJobCaps } = newJobCapsProvider(callWithRequestNonRollupMock); + const response = await newJobCaps(indexPattern, isRollup, savedObjectsClientMock); expect(response).toEqual(farequoteJobCaps); done(); }); @@ -61,8 +57,8 @@ describe('job_service - job_caps', () => { it('can get rollup job caps for non rollup index pattern', async done => { const indexPattern = 'farequote-*'; const isRollup = true; - const { newJobCaps } = newJobCapsProvider(callWithRequestNonRollupMock, requestMock); - const response = await newJobCaps(indexPattern, isRollup); + const { newJobCaps } = newJobCapsProvider(callWithRequestNonRollupMock); + const response = await newJobCaps(indexPattern, isRollup, savedObjectsClientMock); expect(response).toEqual(farequoteJobCapsEmpty); done(); }); @@ -72,8 +68,8 @@ describe('job_service - job_caps', () => { it('can get rollup job caps for rollup index pattern', async done => { const indexPattern = 'cloud_roll_index'; const isRollup = true; - const { newJobCaps } = newJobCapsProvider(callWithRequestRollupMock, requestMock); - const response = await newJobCaps(indexPattern, isRollup); + const { newJobCaps } = newJobCapsProvider(callWithRequestRollupMock); + const response = await newJobCaps(indexPattern, isRollup, savedObjectsClientMock); expect(response).toEqual(cloudwatchJobCaps); done(); }); @@ -81,8 +77,8 @@ describe('job_service - job_caps', () => { it('can get non rollup job caps for rollup index pattern', async done => { const indexPattern = 'cloud_roll_index'; const isRollup = false; - const { newJobCaps } = newJobCapsProvider(callWithRequestRollupMock, requestMock); - const response = await newJobCaps(indexPattern, isRollup); + const { newJobCaps } = newJobCapsProvider(callWithRequestRollupMock); + const response = await newJobCaps(indexPattern, isRollup, savedObjectsClientMock); expect(response).not.toEqual(cloudwatchJobCaps); done(); }); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts index cbb249be09aa0..3a9d979ccb22c 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Request } from 'src/legacy/server/kbn_server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { Aggregation, Field, NewJobCaps } from '../../../../common/types/fields'; import { fieldServiceProvider } from './field_service'; @@ -12,12 +12,18 @@ interface NewJobCapsResponse { [indexPattern: string]: NewJobCaps; } -export function newJobCapsProvider(callWithRequest: any, request: Request) { +export function newJobCapsProvider(callWithRequest: any) { async function newJobCaps( indexPattern: string, - isRollup: boolean = false + isRollup: boolean = false, + savedObjectsClient: SavedObjectsClientContract ): Promise { - const fieldService = fieldServiceProvider(indexPattern, isRollup, callWithRequest, request); + const fieldService = fieldServiceProvider( + indexPattern, + isRollup, + callWithRequest, + savedObjectsClient + ); const { aggs, fields } = await fieldService.getData(); convertForStringify(aggs, fields); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts index 5f8d8ae5c1f25..11b0802192e1f 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Request } from 'src/legacy/server/kbn_server'; import { SavedObject } from 'src/core/server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { FieldId } from '../../../../common/types/fields'; import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; @@ -21,9 +21,9 @@ export interface RollupJob { export async function rollupServiceProvider( indexPattern: string, callWithRequest: any, - request: Request + savedObjectsClient: SavedObjectsClientContract ) { - const rollupIndexPatternObject = await loadRollupIndexPattern(indexPattern, request); + const rollupIndexPatternObject = await loadRollupIndexPattern(indexPattern, savedObjectsClient); let jobIndexPatterns: string[] = [indexPattern]; async function getRollupJobs(): Promise { @@ -57,9 +57,8 @@ export async function rollupServiceProvider( async function loadRollupIndexPattern( indexPattern: string, - request: Request + savedObjectsClient: SavedObjectsClientContract ): Promise { - const savedObjectsClient = request.getSavedObjectsClient(); const resp = await savedObjectsClient.find({ type: 'index-pattern', fields: ['title', 'type', 'typeMeta'], diff --git a/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts index 392d3bfd84768..d728fbf312d76 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts @@ -6,6 +6,18 @@ import { schema } from '@kbn/config-schema'; +const customRulesSchema = schema.maybe( + schema.arrayOf( + schema.maybe( + schema.object({ + actions: schema.arrayOf(schema.string()), + conditions: schema.arrayOf(schema.any()), + scope: schema.maybe(schema.any()), + }) + ) + ) +); + const detectorSchema = schema.object({ identifier: schema.maybe(schema.string()), function: schema.string(), @@ -14,6 +26,7 @@ const detectorSchema = schema.object({ over_field_name: schema.maybe(schema.string()), partition_field_name: schema.maybe(schema.string()), detector_description: schema.maybe(schema.string()), + custom_rules: customRulesSchema, }); const customUrlSchema = { @@ -34,15 +47,8 @@ export const anomalyDetectionUpdateJobSchema = { schema.maybe( schema.object({ detector_index: schema.number(), - custom_rules: schema.arrayOf( - schema.maybe( - schema.object({ - actions: schema.arrayOf(schema.string()), - conditions: schema.arrayOf(schema.any()), - scope: schema.maybe(schema.any()), - }) - ) - ), + description: schema.maybe(schema.string()), + custom_rules: customRulesSchema, }) ) ) diff --git a/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts new file mode 100644 index 0000000000000..b37fcba737802 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +const analyzerSchema = { + tokenizer: schema.string(), + filter: schema.maybe( + schema.arrayOf( + schema.object({ + type: schema.string(), + stopwords: schema.arrayOf(schema.maybe(schema.string())), + }) + ) + ), +}; + +export const categorizationFieldExamplesSchema = { + indexPatternTitle: schema.string(), + query: schema.any(), + size: schema.number(), + field: schema.string(), + timeField: schema.maybe(schema.string()), + start: schema.number(), + end: schema.number(), + analyzer: schema.object(analyzerSchema), +}; + +export const chartSchema = { + indexPatternTitle: schema.string(), + timeField: schema.maybe(schema.string()), + start: schema.maybe(schema.number()), + end: schema.maybe(schema.number()), + intervalMs: schema.number(), + query: schema.any(), + aggFieldNamePairs: schema.arrayOf(schema.any()), + splitFieldName: schema.maybe(schema.nullable(schema.string())), + splitFieldValue: schema.maybe(schema.nullable(schema.string())), +}; + +export const datafeedIdsSchema = { datafeedIds: schema.arrayOf(schema.maybe(schema.string())) }; + +export const forceStartDatafeedSchema = { + datafeedIds: schema.arrayOf(schema.maybe(schema.string())), + start: schema.maybe(schema.number()), + end: schema.maybe(schema.number()), +}; + +export const jobIdsSchema = { + jobIds: schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.maybe(schema.string()))]) + ), +}; + +export const jobsWithTimerangeSchema = { dateFormatTz: schema.maybe(schema.string()) }; + +export const lookBackProgressSchema = { + jobId: schema.string(), + start: schema.maybe(schema.number()), + end: schema.maybe(schema.number()), +}; + +export const topCategoriesSchema = { jobId: schema.string(), count: schema.number() }; + +export const updateGroupsSchema = { + jobs: schema.maybe( + schema.arrayOf( + schema.object({ + job_id: schema.maybe(schema.string()), + groups: schema.arrayOf(schema.maybe(schema.string())), + }) + ) + ), +}; diff --git a/x-pack/legacy/plugins/ml/server/routes/apidoc.json b/x-pack/legacy/plugins/ml/server/routes/apidoc.json index 919592f8ed62a..3fac715fef85a 100644 --- a/x-pack/legacy/plugins/ml/server/routes/apidoc.json +++ b/x-pack/legacy/plugins/ml/server/routes/apidoc.json @@ -50,6 +50,25 @@ "Annotations", "GetAnnotations", "IndexAnnotations", - "DeleteAnnotation" + "DeleteAnnotation", + "JobService", + "ForceStartDatafeeds", + "StopDatafeeds", + "DeleteJobs", + "CloseJobs", + "JobsSummary", + "JobsWithTimerange", + "CreateFullJobsList", + "GetAllGroups", + "UpdateGroups", + "DeletingJobTasks", + "JobsExist", + "NewJobCaps", + "NewJobLineChart", + "NewJobPopulationChart", + "GetAllJobAndGroupIds", + "GetLookBackProgress", + "ValidateCategoryExamples", + "TopCategories" ] } diff --git a/x-pack/legacy/plugins/ml/server/routes/calendars.ts b/x-pack/legacy/plugins/ml/server/routes/calendars.ts index 19d614a4e6a22..8e4e1c4c14751 100644 --- a/x-pack/legacy/plugins/ml/server/routes/calendars.ts +++ b/x-pack/legacy/plugins/ml/server/routes/calendars.ts @@ -13,32 +13,32 @@ import { calendarSchema } from '../new_platform/calendars_schema'; import { CalendarManager, Calendar, FormCalendar } from '../models/calendar'; function getAllCalendars(context: RequestHandlerContext) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.getAllCalendars(); } function getCalendar(context: RequestHandlerContext, calendarId: string) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.getCalendar(calendarId); } function newCalendar(context: RequestHandlerContext, calendar: FormCalendar) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.newCalendar(calendar); } function updateCalendar(context: RequestHandlerContext, calendarId: string, calendar: Calendar) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.updateCalendar(calendarId, calendar); } function deleteCalendar(context: RequestHandlerContext, calendarId: string) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.deleteCalendar(calendarId); } function getCalendarsByIds(context: RequestHandlerContext, calendarIds: string) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.getCalendarsByIds(calendarIds); } diff --git a/x-pack/legacy/plugins/ml/server/routes/job_service.js b/x-pack/legacy/plugins/ml/server/routes/job_service.js deleted file mode 100644 index a83b4fa403f65..0000000000000 --- a/x-pack/legacy/plugins/ml/server/routes/job_service.js +++ /dev/null @@ -1,319 +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 { callWithRequestFactory } from '../client/call_with_request_factory'; -import { wrapError } from '../client/errors'; -import { jobServiceProvider } from '../models/job_service'; - -export function jobServiceRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { - route({ - method: 'POST', - path: '/api/ml/jobs/force_start_datafeeds', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { forceStartDatafeeds } = jobServiceProvider(callWithRequest); - const { datafeedIds, start, end } = request.payload; - return forceStartDatafeeds(datafeedIds, start, end).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/stop_datafeeds', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { stopDatafeeds } = jobServiceProvider(callWithRequest); - const { datafeedIds } = request.payload; - return stopDatafeeds(datafeedIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/delete_jobs', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { deleteJobs } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return deleteJobs(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/close_jobs', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { closeJobs } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return closeJobs(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/jobs_summary', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { jobsSummary } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return jobsSummary(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/jobs_with_timerange', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { jobsWithTimerange } = jobServiceProvider(callWithRequest); - const { dateFormatTz } = request.payload; - return jobsWithTimerange(dateFormatTz).catch(resp => { - wrapError(resp); - }); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/jobs', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { createFullJobsList } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return createFullJobsList(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/jobs/groups', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { getAllGroups } = jobServiceProvider(callWithRequest); - return getAllGroups().catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/update_groups', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { updateGroups } = jobServiceProvider(callWithRequest); - const { jobs } = request.payload; - return updateGroups(jobs).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/jobs/deleting_jobs_tasks', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { deletingJobTasks } = jobServiceProvider(callWithRequest); - return deletingJobTasks().catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/jobs_exist', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { jobsExist } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return jobsExist(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/jobs/new_job_caps/{indexPattern}', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { indexPattern } = request.params; - const isRollup = request.query.rollup === 'true'; - const { newJobCaps } = jobServiceProvider(callWithRequest, request); - return newJobCaps(indexPattern, isRollup).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/new_job_line_chart', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName, - splitFieldValue, - } = request.payload; - const { newJobLineChart } = jobServiceProvider(callWithRequest, request); - return newJobLineChart( - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName, - splitFieldValue - ).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/new_job_population_chart', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName, - } = request.payload; - const { newJobPopulationChart } = jobServiceProvider(callWithRequest, request); - return newJobPopulationChart( - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName - ).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/jobs/all_jobs_and_group_ids', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { getAllJobAndGroupIds } = jobServiceProvider(callWithRequest); - return getAllJobAndGroupIds().catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/look_back_progress', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { getLookBackProgress } = jobServiceProvider(callWithRequest); - const { jobId, start, end } = request.payload; - return getLookBackProgress(jobId, start, end).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/categorization_field_examples', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { validateCategoryExamples } = jobServiceProvider(callWithRequest); - const { - indexPatternTitle, - timeField, - query, - size, - field, - start, - end, - analyzer, - } = request.payload; - return validateCategoryExamples( - indexPatternTitle, - query, - size, - field, - timeField, - start, - end, - analyzer - ).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/top_categories', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { topCategories } = jobServiceProvider(callWithRequest); - const { jobId, count } = request.payload; - return topCategories(jobId, count).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); -} diff --git a/x-pack/legacy/plugins/ml/server/routes/job_service.ts b/x-pack/legacy/plugins/ml/server/routes/job_service.ts new file mode 100644 index 0000000000000..3af651c92353b --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/job_service.ts @@ -0,0 +1,610 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { schema } from '@kbn/config-schema'; +import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { wrapError } from '../client/error_wrapper'; +import { RouteInitialization } from '../new_platform/plugin'; +import { + categorizationFieldExamplesSchema, + chartSchema, + datafeedIdsSchema, + forceStartDatafeedSchema, + jobIdsSchema, + jobsWithTimerangeSchema, + lookBackProgressSchema, + topCategoriesSchema, + updateGroupsSchema, +} from '../new_platform/job_service_schema'; +// @ts-ignore no declaration module +import { jobServiceProvider } from '../models/job_service'; + +/** + * Routes for job service + */ +export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitialization) { + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/force_start_datafeeds + * @apiName ForceStartDatafeeds + * @apiDescription Starts one or more datafeeds + */ + router.post( + { + path: '/api/ml/jobs/force_start_datafeeds', + validate: { + body: schema.object(forceStartDatafeedSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { forceStartDatafeeds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { datafeedIds, start, end } = request.body; + const resp = await forceStartDatafeeds(datafeedIds, start, end); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/stop_datafeeds + * @apiName StopDatafeeds + * @apiDescription Stops one or more datafeeds + */ + router.post( + { + path: '/api/ml/jobs/stop_datafeeds', + validate: { + body: schema.object(datafeedIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { stopDatafeeds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { datafeedIds } = request.body; + const resp = await stopDatafeeds(datafeedIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/delete_jobs + * @apiName DeleteJobs + * @apiDescription Deletes an existing anomaly detection job + */ + router.post( + { + path: '/api/ml/jobs/delete_jobs', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { deleteJobs } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await deleteJobs(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/close_jobs + * @apiName CloseJobs + * @apiDescription Closes one or more anomaly detection jobs + */ + router.post( + { + path: '/api/ml/jobs/close_jobs', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { closeJobs } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await closeJobs(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs_summary + * @apiName JobsSummary + * @apiDescription Creates a summary jobs list. Jobs include job stats, datafeed stats, and calendars. + */ + router.post( + { + path: '/api/ml/jobs/jobs_summary', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { jobsSummary } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await jobsSummary(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs_with_time_range + * @apiName JobsWithTimerange + * @apiDescription Creates a list of jobs with data about the job's timerange + */ + router.post( + { + path: '/api/ml/jobs/jobs_with_time_range', + validate: { + body: schema.object(jobsWithTimerangeSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { jobsWithTimerange } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { dateFormatTz } = request.body; + const resp = await jobsWithTimerange(dateFormatTz); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs + * @apiName CreateFullJobsList + * @apiDescription Creates a list of jobs + */ + router.post( + { + path: '/api/ml/jobs/jobs', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { createFullJobsList } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await createFullJobsList(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {get} /api/ml/jobs/groups + * @apiName GetAllGroups + * @apiDescription Returns array of group objects with job ids listed for each group + */ + router.get( + { + path: '/api/ml/jobs/groups', + validate: false, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { getAllGroups } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const resp = await getAllGroups(); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/update_groups + * @apiName UpdateGroups + * @apiDescription Updates 'groups' property of an anomaly detection job + */ + router.post( + { + path: '/api/ml/jobs/update_groups', + validate: { + body: schema.object(updateGroupsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { updateGroups } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobs } = request.body; + const resp = await updateGroups(jobs); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {get} /api/ml/jobs/deleting_jobs_tasks + * @apiName DeletingJobTasks + * @apiDescription Gets the ids of deleting anomaly detection jobs + */ + router.get( + { + path: '/api/ml/jobs/deleting_jobs_tasks', + validate: false, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { deletingJobTasks } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const resp = await deletingJobTasks(); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs_exist + * @apiName JobsExist + * @apiDescription Checks if each of the jobs in the specified list of IDs exist + */ + router.post( + { + path: '/api/ml/jobs/jobs_exist', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { jobsExist } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await jobsExist(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {get} /api/ml/jobs/new_job_caps/:indexPattern + * @apiName NewJobCaps + * @apiDescription Retrieve the capabilities of fields for indices + */ + router.get( + { + path: '/api/ml/jobs/new_job_caps/{indexPattern}', + validate: { + params: schema.object({ indexPattern: schema.string() }), + query: schema.maybe(schema.object({ rollup: schema.maybe(schema.string()) })), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { indexPattern } = request.params; + const isRollup = request.query.rollup === 'true'; + const savedObjectsClient = context.core.savedObjects.client; + const { newJobCaps } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const resp = await newJobCaps(indexPattern, isRollup, savedObjectsClient); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/new_job_line_chart + * @apiName NewJobLineChart + * @apiDescription Returns line chart data for anomaly detection job + */ + router.post( + { + path: '/api/ml/jobs/new_job_line_chart', + validate: { + body: schema.object(chartSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + splitFieldValue, + } = request.body; + + const { newJobLineChart } = jobServiceProvider( + context.ml!.mlClient.callAsCurrentUser, + request + ); + const resp = await newJobLineChart( + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + splitFieldValue + ); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/new_job_population_chart + * @apiName NewJobPopulationChart + * @apiDescription Returns population job chart data + */ + router.post( + { + path: '/api/ml/jobs/new_job_population_chart', + validate: { + body: schema.object(chartSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + } = request.body; + + const { newJobPopulationChart } = jobServiceProvider( + context.ml!.mlClient.callAsCurrentUser + ); + const resp = await newJobPopulationChart( + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName + ); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {get} /api/ml/jobs/all_jobs_and_group_ids + * @apiName GetAllJobAndGroupIds + * @apiDescription Returns a list of all job IDs and all group IDs + */ + router.get( + { + path: '/api/ml/jobs/all_jobs_and_group_ids', + validate: false, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { getAllJobAndGroupIds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const resp = await getAllJobAndGroupIds(); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/look_back_progress + * @apiName GetLookBackProgress + * @apiDescription Returns current progress of anomaly detection job + */ + router.post( + { + path: '/api/ml/jobs/look_back_progress', + validate: { + body: schema.object(lookBackProgressSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { getLookBackProgress } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobId, start, end } = request.body; + const resp = await getLookBackProgress(jobId, start, end); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/categorization_field_examples + * @apiName ValidateCategoryExamples + * @apiDescription Validates category examples + */ + router.post( + { + path: '/api/ml/jobs/categorization_field_examples', + validate: { + body: schema.object(categorizationFieldExamplesSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { validateCategoryExamples } = jobServiceProvider( + context.ml!.mlClient.callAsCurrentUser + ); + const { + indexPatternTitle, + timeField, + query, + size, + field, + start, + end, + analyzer, + } = request.body; + + const resp = await validateCategoryExamples( + indexPatternTitle, + query, + size, + field, + timeField, + start, + end, + analyzer + ); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/top_categories + * @apiName TopCategories + * @apiDescription Returns list of top categories + */ + router.post( + { + path: '/api/ml/jobs/top_categories', + validate: { + body: schema.object(topCategoriesSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { topCategories } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobId, count } = request.body; + const resp = await topCategories(jobId, count); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index fd89c40f010b7..18b815fb429cb 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -115,9 +115,6 @@ export const security = kibana => const xpackInfo = server.plugins.xpack_main.info; securityPlugin.__legacyCompat.registerLegacyAPI({ auditLogger: new AuditLogger(server, 'security', config, xpackInfo), - isSystemAPIRequest: server.plugins.kibana.systemApi.isSystemApiRequest.bind( - server.plugins.kibana.systemApi - ), }); // Legacy xPack Info endpoint returns whatever we return in a callback for `registerLicenseCheckResultsGenerator` diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index 0a3e447ac64a1..c786dad61c09d 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -5,12 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { get } from 'lodash/fp'; import { resolve } from 'path'; import { Server } from 'hapi'; import { Root } from 'joi'; -import { PluginInitializerContext } from '../../../../src/core/server'; import { plugin } from './server'; import { savedObjectMappings } from './server/saved_objects'; @@ -32,7 +30,6 @@ import { SIGNALS_INDEX_KEY, } from './common/constants'; import { defaultIndexPattern } from './default_index_pattern'; -import { initServerWithKibana } from './server/kibana.index'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -151,27 +148,20 @@ export const siem = (kibana: any) => { mappings: savedObjectMappings, }, init(server: Server) { - const { config, newPlatform, plugins, route } = server; - const { coreContext, env, setup } = newPlatform; - const initializerContext = { ...coreContext, env } as PluginInitializerContext; - const serverFacade = { - config, - usingEphemeralEncryptionKey: - get('usingEphemeralEncryptionKey', newPlatform.setup.plugins.encryptedSavedObjects) ?? - false, - plugins: { - alerting: plugins.alerting, - actions: newPlatform.start.plugins.actions, - elasticsearch: plugins.elasticsearch, - spaces: plugins.spaces, - savedObjects: server.savedObjects.SavedObjectsClient, - }, - route: route.bind(server), + const { coreContext, env, setup, start } = server.newPlatform; + const initializerContext = { ...coreContext, env }; + const __legacy = { + config: server.config, + alerting: server.plugins.alerting, + route: server.route.bind(server), }; - // @ts-ignore-next-line: setup.plugins is too loosely typed - plugin(initializerContext).setup(setup.core, setup.plugins); - initServerWithKibana(initializerContext, serverFacade); + // @ts-ignore-next-line: NewPlatform shim is too loosely typed + const pluginInstance = plugin(initializerContext); + // @ts-ignore-next-line: NewPlatform shim is too loosely typed + pluginInstance.setup(setup.core, setup.plugins, __legacy); + // @ts-ignore-next-line: NewPlatform shim is too loosely typed + pluginInstance.start(start.core, start.plugins); }, config(Joi: Root) { // See x-pack/plugins/siem/server/config.ts if you're adding another diff --git a/x-pack/legacy/plugins/siem/public/components/header_page_new/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_page_new/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..1b792503cf1c6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/header_page_new/__snapshots__/index.test.tsx.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HeaderPage it renders 1`] = ` + + + + + + Test title + + + + + + + + + + Test supplement + + + + +`; diff --git a/x-pack/legacy/plugins/siem/public/components/header_page_new/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_page_new/index.test.tsx new file mode 100644 index 0000000000000..83a70fd90d82b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/header_page_new/index.test.tsx @@ -0,0 +1,224 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { shallow } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../mock'; +import { HeaderPage } from './index'; +import { useMountAppended } from '../../utils/use_mount_appended'; + +describe('HeaderPage', () => { + const mount = useMountAppended(); + + test('it renders', () => { + const wrapper = shallow( + + {'Test supplement'} + + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the title', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-title"]') + .first() + .exists() + ).toBe(true); + }); + + test('it renders the back link when provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('.siemHeaderPage__linkBack') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render the back link when not provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('.siemHeaderPage__linkBack') + .first() + .exists() + ).toBe(false); + }); + + test('it renders the first subtitle when provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-subtitle"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render the first subtitle when not provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-section-subtitle"]') + .first() + .exists() + ).toBe(false); + }); + + test('it renders the second subtitle when provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-subtitle-2"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render the second subtitle when not provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-section-subtitle-2"]') + .first() + .exists() + ).toBe(false); + }); + + test('it renders supplements when children provided', () => { + const wrapper = mount( + + + {'Test supplement'} + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-supplements"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render supplements when children not provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-supplements"]') + .first() + .exists() + ).toBe(false); + }); + + test('it applies border styles when border is true', () => { + const wrapper = mount( + + + + ); + const siemHeaderPage = wrapper.find('.siemHeaderPage').first(); + + expect(siemHeaderPage).toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); + expect(siemHeaderPage).toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); + }); + + test('it DOES NOT apply border styles when border is false', () => { + const wrapper = mount( + + + + ); + const siemHeaderPage = wrapper.find('.siemHeaderPage').first(); + + expect(siemHeaderPage).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); + expect(siemHeaderPage).not.toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); + }); + + test('it renders as a draggable when arguments provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-draggable"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render as a draggable when arguments not provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-draggable"]') + .first() + .exists() + ).toBe(false); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/header_page_new/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_page_new/index.tsx new file mode 100644 index 0000000000000..7e486c78fb9b9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/header_page_new/index.tsx @@ -0,0 +1,220 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + EuiBadge, + EuiBetaBadge, + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiTitle, +} from '@elastic/eui'; +import React from 'react'; +import styled, { css } from 'styled-components'; + +import { DefaultDraggable } from '../draggables'; +import { LinkIcon, LinkIconProps } from '../link_icon'; +import { Subtitle, SubtitleProps } from '../subtitle'; +import * as i18n from './translations'; + +interface HeaderProps { + border?: boolean; + isLoading?: boolean; +} + +const Header = styled.header.attrs({ + className: 'siemHeaderPage', +})` + ${({ border, theme }) => css` + margin-bottom: ${theme.eui.euiSizeL}; + + ${border && + css` + border-bottom: ${theme.eui.euiBorderThin}; + padding-bottom: ${theme.eui.paddingSizes.l}; + .euiProgress { + top: ${theme.eui.paddingSizes.l}; + } + `} + `} +`; +Header.displayName = 'Header'; + +const FlexItem = styled(EuiFlexItem)` + display: block; +`; +FlexItem.displayName = 'FlexItem'; + +const LinkBack = styled.div.attrs({ + className: 'siemHeaderPage__linkBack', +})` + ${({ theme }) => css` + font-size: ${theme.eui.euiFontSizeXS}; + line-height: ${theme.eui.euiLineHeight}; + margin-bottom: ${theme.eui.euiSizeS}; + `} +`; +LinkBack.displayName = 'LinkBack'; + +const Badge = styled(EuiBadge)` + letter-spacing: 0; +`; +Badge.displayName = 'Badge'; + +const StyledEuiBetaBadge = styled(EuiBetaBadge)` + vertical-align: middle; +`; + +StyledEuiBetaBadge.displayName = 'StyledEuiBetaBadge'; + +const StyledEuiButtonIcon = styled(EuiButtonIcon)` + ${({ theme }) => css` + margin-left: ${theme.eui.euiSize}; + `} +`; + +StyledEuiButtonIcon.displayName = 'StyledEuiButtonIcon'; + +interface BackOptions { + href: LinkIconProps['href']; + text: LinkIconProps['children']; +} + +interface BadgeOptions { + beta?: boolean; + text: string; + tooltip?: string; +} + +interface DraggableArguments { + field: string; + value: string; +} +interface IconAction { + 'aria-label': string; + iconType: string; + onChange: (a: string) => void; + onClick: (b: boolean) => void; + onSubmit: () => void; +} + +export interface HeaderPageProps extends HeaderProps { + backOptions?: BackOptions; + badgeOptions?: BadgeOptions; + children?: React.ReactNode; + draggableArguments?: DraggableArguments; + isEditTitle?: boolean; + iconAction?: IconAction; + subtitle2?: SubtitleProps['items']; + subtitle?: SubtitleProps['items']; + title: string | React.ReactNode; +} + +const HeaderPageComponent: React.FC = ({ + backOptions, + badgeOptions, + border, + children, + draggableArguments, + isEditTitle, + iconAction, + isLoading, + subtitle, + subtitle2, + title, + ...rest +}) => ( + + + + {backOptions && ( + + + {backOptions.text} + + + )} + + {isEditTitle && iconAction ? ( + + + iconAction.onChange(e.target.value)} + value={`${title}`} + /> + + + + + {i18n.SUBMIT} + + + + iconAction.onClick(false)}> + {i18n.CANCEL} + + + + + + ) : ( + + + {!draggableArguments ? ( + title + ) : ( + + )} + {badgeOptions && ( + <> + {' '} + {badgeOptions.beta ? ( + + ) : ( + {badgeOptions.text} + )} + > + )} + {iconAction && ( + iconAction.onClick(true)} + /> + )} + + + )} + + {subtitle && } + {subtitle2 && } + {border && isLoading && } + + + {children && {children}} + + +); + +export const HeaderPage = React.memo(HeaderPageComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/header_page_new/translations.ts b/x-pack/legacy/plugins/siem/public/components/header_page_new/translations.ts new file mode 100644 index 0000000000000..57b2cda0b0b01 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/header_page_new/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const SUBMIT = i18n.translate('xpack.siem.case.casePage.title.submit', { + defaultMessage: 'Submit', +}); + +export const CANCEL = i18n.translate('xpack.siem.case.casePage.title.cancel', { + defaultMessage: 'Cancel', +}); diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts index ad6147e5aad76..c93b415e017bb 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts @@ -13,3 +13,10 @@ export { getOverviewUrl, RedirectToOverviewPage } from './redirect_to_overview'; export { getHostDetailsUrl, getHostsUrl } from './redirect_to_hosts'; export { getNetworkUrl, getIPDetailsUrl, RedirectToNetworkPage } from './redirect_to_network'; export { getTimelinesUrl, RedirectToTimelinesPage } from './redirect_to_timelines'; +export { + getCaseDetailsUrl, + getCaseUrl, + getCreateCaseUrl, + RedirectToCasePage, + RedirectToCreatePage, +} from './redirect_to_case'; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index dc8c696301611..c08b429dc4625 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -20,6 +20,7 @@ import { RedirectToHostsPage, RedirectToHostDetailsPage } from './redirect_to_ho import { RedirectToNetworkPage } from './redirect_to_network'; import { RedirectToOverviewPage } from './redirect_to_overview'; import { RedirectToTimelinesPage } from './redirect_to_timelines'; +import { RedirectToCasePage, RedirectToCreatePage } from './redirect_to_case'; import { DetectionEngineTab } from '../../pages/detection_engine/types'; interface LinkToPageProps { @@ -32,6 +33,20 @@ export const LinkToPage = React.memo(({ match }) => ( component={RedirectToOverviewPage} path={`${match.url}/:pageName(${SiemPageName.overview})`} /> + + + ; + +export const RedirectToCasePage = ({ + match: { + params: { detailName }, + }, +}: CaseComponentProps) => ( + +); + +export const RedirectToCreatePage = () => ; + +const baseCaseUrl = `#/link-to/${SiemPageName.case}`; + +export const getCaseUrl = () => baseCaseUrl; +export const getCaseDetailsUrl = (detailName: string) => `${baseCaseUrl}/${detailName}`; +export const getCreateCaseUrl = () => `${baseCaseUrl}/create`; diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.tsx index e122b3e235a9e..4f74f9ff2f5d6 100644 --- a/x-pack/legacy/plugins/siem/public/components/links/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/links/index.tsx @@ -8,7 +8,12 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; import { encodeIpv6 } from '../../lib/helpers'; -import { getHostDetailsUrl, getIPDetailsUrl } from '../link_to'; +import { + getCaseDetailsUrl, + getHostDetailsUrl, + getIPDetailsUrl, + getCreateCaseUrl, +} from '../link_to'; import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; // Internal Links @@ -35,6 +40,23 @@ const IPDetailsLinkComponent: React.FC<{ export const IPDetailsLink = React.memo(IPDetailsLinkComponent); +const CaseDetailsLinkComponent: React.FC<{ children?: React.ReactNode; detailName: string }> = ({ + children, + detailName, +}) => ( + + {children ? children : detailName} + +); +export const CaseDetailsLink = React.memo(CaseDetailsLinkComponent); +CaseDetailsLink.displayName = 'CaseDetailsLink'; + +export const CreateCaseLink = React.memo<{ children: React.ReactNode }>(({ children }) => ( + {children} +)); + +CreateCaseLink.displayName = 'CreateCaseLink'; + // External Links export const GoogleLink = React.memo<{ children?: React.ReactNode; link: string }>( ({ children, link }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts index e8d5032fd7548..e25fb4374bb14 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts @@ -11,6 +11,7 @@ import { APP_NAME } from '../../../../common/constants'; import { StartServices } from '../../../plugin'; import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/details/utils'; import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; +import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../pages/case/utils'; import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../pages/detection_engine/rules/utils'; import { SiemPageName } from '../../../pages/home/types'; import { RouteSpyState, HostRouteSpyState, NetworkRouteSpyState } from '../../../utils/route/types'; @@ -43,6 +44,9 @@ const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpySt const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => spyState != null && spyState.pageName === SiemPageName.hosts; +const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState => + spyState != null && spyState.pageName === SiemPageName.case; + const isDetectionsRoutes = (spyState: RouteSpyState) => spyState != null && spyState.pageName === SiemPageName.detections; @@ -102,6 +106,9 @@ export const getBreadcrumbsForRoute = ( ), ]; } + if (isCaseRoutes(spyState) && object.navTabs) { + return [...siemRootBreadcrumb, ...getCaseDetailsBreadcrumbs(spyState)]; + } if ( spyState != null && object.navTabs && diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx index ac7a4a0ee52b7..8eb08bd3d62f0 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx @@ -66,6 +66,13 @@ describe('SIEM Navigation', () => { { detailName: undefined, navTabs: { + case: { + disabled: true, + href: '#/link-to/case', + id: 'case', + name: 'Case', + urlKey: 'case', + }, detections: { disabled: false, href: '#/link-to/detections', @@ -152,6 +159,13 @@ describe('SIEM Navigation', () => { detailName: undefined, filters: [], navTabs: { + case: { + disabled: true, + href: '#/link-to/case', + id: 'case', + name: 'Case', + urlKey: 'case', + }, detections: { disabled: false, href: '#/link-to/detections', diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx b/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx index 5a3439d53dd89..15e58f3efd21e 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx @@ -32,8 +32,6 @@ const TextArea = styled(EuiTextArea)<{ height: number }>` TextArea.displayName = 'TextArea'; -TextArea.displayName = 'TextArea'; - /** An input for entering a new note */ export const NewNote = React.memo<{ noteInputHeight: number; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts b/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts index 22e8f99658f8d..b6ef3c8ccd4e9 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts @@ -6,6 +6,8 @@ export enum CONSTANTS { appQuery = 'query', + caseDetails = 'case.details', + casePage = 'case.page', detectionsPage = 'detections.page', filters = 'filters', hostsDetails = 'hosts.details', @@ -14,10 +16,10 @@ export enum CONSTANTS { networkPage = 'network.page', overviewPage = 'overview.page', savedQuery = 'savedQuery', + timeline = 'timeline', timelinePage = 'timeline.page', timerange = 'timerange', - timeline = 'timeline', unknown = 'unknown', } -export type UrlStateType = 'detections' | 'host' | 'network' | 'overview' | 'timeline'; +export type UrlStateType = 'case' | 'detections' | 'host' | 'network' | 'overview' | 'timeline'; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts index 7be775ef0c0e4..05329621aa97a 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts @@ -98,6 +98,8 @@ export const getUrlType = (pageName: string): UrlStateType => { return 'detections'; } else if (pageName === SiemPageName.timelines) { return 'timeline'; + } else if (pageName === SiemPageName.case) { + return 'case'; } return 'overview'; }; @@ -131,6 +133,11 @@ export const getCurrentLocation = ( return CONSTANTS.detectionsPage; } else if (pageName === SiemPageName.timelines) { return CONSTANTS.timelinePage; + } else if (pageName === SiemPageName.case) { + if (detailName != null) { + return CONSTANTS.caseDetails; + } + return CONSTANTS.casePage; } return CONSTANTS.unknown; }; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts index fea1bc016fd49..97979e514aeaf 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts @@ -60,9 +60,12 @@ export const URL_STATE_KEYS: Record = { CONSTANTS.timeline, ], timeline: [CONSTANTS.timeline, CONSTANTS.timerange], + case: [], }; export type LocationTypes = + | CONSTANTS.caseDetails + | CONSTANTS.casePage | CONSTANTS.detectionsPage | CONSTANTS.hostsDetails | CONSTANTS.hostsPage diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts new file mode 100644 index 0000000000000..830e00c70975e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaServices } from '../../lib/kibana'; +import { AllCases, FetchCasesProps, Case, NewCase, SortFieldCase } from './types'; +import { Direction } from '../../graphql/types'; +import { throwIfNotOk } from '../../hooks/api/api'; +import { CASES_URL } from './constants'; + +export const getCase = async (caseId: string, includeComments: boolean) => { + const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, { + method: 'GET', + asResponse: true, + query: { + includeComments, + }, + }); + await throwIfNotOk(response.response); + return response.body!; +}; + +export const getCases = async ({ + filterOptions = { + search: '', + tags: [], + }, + queryParams = { + page: 1, + perPage: 20, + sortField: SortFieldCase.createdAt, + sortOrder: Direction.desc, + }, +}: FetchCasesProps): Promise => { + const tags = [...(filterOptions.tags?.map(t => `case-workflow.attributes.tags: ${t}`) ?? [])]; + const query = { + ...queryParams, + filter: tags.join(' AND '), + search: filterOptions.search, + }; + const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, { + method: 'GET', + query, + asResponse: true, + }); + await throwIfNotOk(response.response); + return response.body!; +}; + +export const createCase = async (newCase: NewCase): Promise => { + const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, { + method: 'POST', + asResponse: true, + body: JSON.stringify(newCase), + }); + await throwIfNotOk(response.response); + return response.body!; +}; + +export const updateCaseProperty = async ( + caseId: string, + updatedCase: Partial +): Promise> => { + const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, { + method: 'PATCH', + asResponse: true, + body: JSON.stringify(updatedCase), + }); + await throwIfNotOk(response.response); + return response.body!; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts new file mode 100644 index 0000000000000..c8d668527ae32 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.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. + */ + +export const CASES_URL = `/api/cases`; +export const DEFAULT_TABLE_ACTIVE_PAGE = 1; +export const DEFAULT_TABLE_LIMIT = 5; +export const FETCH_FAILURE = 'FETCH_FAILURE'; +export const FETCH_INIT = 'FETCH_INIT'; +export const FETCH_SUCCESS = 'FETCH_SUCCESS'; +export const POST_NEW_CASE = 'POST_NEW_CASE'; +export const UPDATE_CASE_PROPERTY = 'UPDATE_CASE_PROPERTY'; +export const UPDATE_FILTER_OPTIONS = 'UPDATE_FILTER_OPTIONS'; +export const UPDATE_QUERY_PARAMS = 'UPDATE_QUERY_PARAMS'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts new file mode 100644 index 0000000000000..0c8b896e2b426 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_TITLE = i18n.translate('xpack.siem.containers.case.errorTitle', { + defaultMessage: 'Error fetching data', +}); + +export const TAG_FETCH_FAILURE = i18n.translate( + 'xpack.siem.containers.case.tagFetchFailDescription', + { + defaultMessage: 'Failed to fetch Tags', + } +); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts new file mode 100644 index 0000000000000..0f80b2327a30c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Direction } from '../../graphql/types'; +interface FormData { + isNew?: boolean; +} + +export interface NewCase extends FormData { + description: string; + tags: string[]; + title: string; +} + +export interface Case { + case_id: string; + created_at: string; + created_by: ElasticUser; + description: string; + state: string; + tags: string[]; + title: string; + updated_at: string; +} + +export interface QueryParams { + page: number; + perPage: number; + sortField: SortFieldCase; + sortOrder: Direction; +} + +export interface FilterOptions { + search: string; + tags: string[]; +} + +export interface AllCases { + cases: Case[]; + page: number; + per_page: number; + total: number; +} +export enum SortFieldCase { + createdAt = 'created_at', + state = 'state', + updatedAt = 'updated_at', +} + +export interface ElasticUser { + readonly username: string; + readonly full_name?: string; +} + +export interface FetchCasesProps { + queryParams?: QueryParams; + filterOptions?: FilterOptions; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx new file mode 100644 index 0000000000000..8cc961c68fdf0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { useEffect, useReducer } from 'react'; + +import { Case } from './types'; +import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS } from './constants'; +import { getTypedPayload } from './utils'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { useStateToaster } from '../../components/toasters'; +import { getCase } from './api'; + +interface CaseState { + data: Case; + isLoading: boolean; + isError: boolean; +} +interface Action { + type: string; + payload?: Case; +} + +const dataFetchReducer = (state: CaseState, action: Action): CaseState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case FETCH_SUCCESS: + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; +const initialData: Case = { + case_id: '', + created_at: '', + created_by: { + username: '', + }, + description: '', + state: '', + tags: [], + title: '', + updated_at: '', +}; + +export const useGetCase = (caseId: string): [CaseState] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: true, + isError: false, + data: initialData, + }); + const [, dispatchToaster] = useStateToaster(); + + const callFetch = () => { + let didCancel = false; + const fetchData = async () => { + dispatch({ type: FETCH_INIT }); + try { + const response = await getCase(caseId, false); + if (!didCancel) { + dispatch({ type: FETCH_SUCCESS, payload: response }); + } + } catch (error) { + if (!didCancel) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + }; + }; + + useEffect(() => { + callFetch(); + }, [caseId]); + return [state]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx new file mode 100644 index 0000000000000..db9c07747ba04 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; +import { isEqual } from 'lodash/fp'; +import { + DEFAULT_TABLE_ACTIVE_PAGE, + DEFAULT_TABLE_LIMIT, + FETCH_FAILURE, + FETCH_INIT, + FETCH_SUCCESS, + UPDATE_QUERY_PARAMS, + UPDATE_FILTER_OPTIONS, +} from './constants'; +import { AllCases, SortFieldCase, FilterOptions, QueryParams } from './types'; +import { getTypedPayload } from './utils'; +import { Direction } from '../../graphql/types'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import { useStateToaster } from '../../components/toasters'; +import * as i18n from './translations'; +import { getCases } from './api'; + +export interface UseGetCasesState { + data: AllCases; + isLoading: boolean; + isError: boolean; + queryParams: QueryParams; + filterOptions: FilterOptions; +} + +export interface QueryArgs { + page?: number; + perPage?: number; + sortField?: SortFieldCase; + sortOrder?: Direction; +} + +export interface Action { + type: string; + payload?: AllCases | QueryArgs | FilterOptions; +} +const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case FETCH_SUCCESS: + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + case UPDATE_QUERY_PARAMS: + return { + ...state, + queryParams: { + ...state.queryParams, + ...action.payload, + }, + }; + case UPDATE_FILTER_OPTIONS: + return { + ...state, + filterOptions: getTypedPayload(action.payload), + }; + default: + throw new Error(); + } +}; + +const initialData: AllCases = { + page: 0, + per_page: 0, + total: 0, + cases: [], +}; +export const useGetCases = (): [ + UseGetCasesState, + Dispatch>, + Dispatch> +] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + filterOptions: { + search: '', + tags: [], + }, + queryParams: { + page: DEFAULT_TABLE_ACTIVE_PAGE, + perPage: DEFAULT_TABLE_LIMIT, + sortField: SortFieldCase.createdAt, + sortOrder: Direction.desc, + }, + }); + const [queryParams, setQueryParams] = useState(state.queryParams as QueryArgs); + const [filterQuery, setFilters] = useState(state.filterOptions as FilterOptions); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + if (!isEqual(queryParams, state.queryParams)) { + dispatch({ type: UPDATE_QUERY_PARAMS, payload: queryParams }); + } + }, [queryParams, state.queryParams]); + + useEffect(() => { + if (!isEqual(filterQuery, state.filterOptions)) { + dispatch({ type: UPDATE_FILTER_OPTIONS, payload: filterQuery }); + } + }, [filterQuery, state.filterOptions]); + + useEffect(() => { + let didCancel = false; + const fetchData = async () => { + dispatch({ type: FETCH_INIT }); + try { + const response = await getCases({ + filterOptions: state.filterOptions, + queryParams: state.queryParams, + }); + if (!didCancel) { + dispatch({ + type: FETCH_SUCCESS, + payload: response, + }); + } + } catch (error) { + if (!didCancel) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + }; + }, [state.queryParams, state.filterOptions]); + return [state, setQueryParams, setFilters]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx new file mode 100644 index 0000000000000..f796ae550c9ec --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useReducer } from 'react'; +import chrome from 'ui/chrome'; +import { useStateToaster } from '../../components/toasters'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; +import { throwIfNotOk } from '../../hooks/api/api'; + +interface TagsState { + data: string[]; + isLoading: boolean; + isError: boolean; +} +interface Action { + type: string; + payload?: string[]; +} + +const dataFetchReducer = (state: TagsState, action: Action): TagsState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case FETCH_SUCCESS: + const getTypedPayload = (a: Action['payload']) => a as string[]; + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; +const initialData: string[] = []; + +export const useGetTags = (): [TagsState] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + }); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let didCancel = false; + const fetchData = async () => { + dispatch({ type: FETCH_INIT }); + try { + const response = await fetch(`${chrome.getBasePath()}/api/cases/tags`, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-system-api': 'true', + }, + }); + if (!didCancel) { + await throwIfNotOk(response); + const responseJson = await response.json(); + dispatch({ type: FETCH_SUCCESS, payload: responseJson }); + } + } catch (error) { + if (!didCancel) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + }; + }, []); + return [state]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx new file mode 100644 index 0000000000000..5cf99701977d2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; +import { useStateToaster } from '../../components/toasters'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, POST_NEW_CASE } from './constants'; +import { Case, NewCase } from './types'; +import { createCase } from './api'; +import { getTypedPayload } from './utils'; + +interface NewCaseState { + data: NewCase; + newCase?: Case; + isLoading: boolean; + isError: boolean; +} +interface Action { + type: string; + payload?: NewCase | Case; +} + +const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case POST_NEW_CASE: + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload(action.payload), + }; + case FETCH_SUCCESS: + return { + ...state, + isLoading: false, + isError: false, + newCase: getTypedPayload(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; +const initialData: NewCase = { + description: '', + isNew: false, + tags: [], + title: '', +}; + +export const usePostCase = (): [NewCaseState, Dispatch>] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + }); + const [formData, setFormData] = useState(initialData); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + dispatch({ type: POST_NEW_CASE, payload: formData }); + }, [formData]); + + useEffect(() => { + const postCase = async () => { + dispatch({ type: FETCH_INIT }); + try { + const dataWithoutIsNew = state.data; + delete dataWithoutIsNew.isNew; + const response = await createCase(dataWithoutIsNew); + dispatch({ type: FETCH_SUCCESS, payload: response }); + } catch (error) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + }; + if (state.data.isNew) { + postCase(); + } + }, [state.data.isNew]); + return [state, setFormData]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx new file mode 100644 index 0000000000000..68592c17e58dc --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { useEffect, useReducer } from 'react'; +import { useStateToaster } from '../../components/toasters'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, UPDATE_CASE_PROPERTY } from './constants'; +import { Case } from './types'; +import { updateCaseProperty } from './api'; +import { getTypedPayload } from './utils'; + +type UpdateKey = keyof Case; + +interface NewCaseState { + data: Case; + isLoading: boolean; + isError: boolean; + updateKey?: UpdateKey | null; +} + +interface UpdateByKey { + updateKey: UpdateKey; + updateValue: Case[UpdateKey]; +} + +interface Action { + type: string; + payload?: Partial | UpdateByKey; +} + +const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + updateKey: null, + }; + case UPDATE_CASE_PROPERTY: + const { updateKey, updateValue } = getTypedPayload(action.payload); + return { + ...state, + isLoading: false, + isError: false, + data: { + ...state.data, + [updateKey]: updateValue, + }, + updateKey, + }; + case FETCH_SUCCESS: + return { + ...state, + isLoading: false, + isError: false, + data: { + ...state.data, + ...getTypedPayload(action.payload), + }, + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; + +export const useUpdateCase = ( + caseId: string, + initialData: Case +): [{ data: Case }, (updates: UpdateByKey) => void] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + }); + const [, dispatchToaster] = useStateToaster(); + + const dispatchUpdateCaseProperty = ({ updateKey, updateValue }: UpdateByKey) => { + dispatch({ + type: UPDATE_CASE_PROPERTY, + payload: { updateKey, updateValue }, + }); + }; + + useEffect(() => { + const updateData = async (updateKey: keyof Case) => { + dispatch({ type: FETCH_INIT }); + try { + const response = await updateCaseProperty(caseId, { [updateKey]: state.data[updateKey] }); + dispatch({ type: FETCH_SUCCESS, payload: response }); + } catch (error) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + }; + if (state.updateKey) { + updateData(state.updateKey); + } + }, [state.updateKey]); + + return [{ data: state.data }, dispatchUpdateCaseProperty]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/utils.ts b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts new file mode 100644 index 0000000000000..8e6eaca1a8f0c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const getTypedPayload = (a: unknown): T => a as T; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx new file mode 100644 index 0000000000000..1206ec950deed --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiButton, EuiFlexGroup } from '@elastic/eui'; +import { HeaderPage } from '../../components/header_page'; +import { WrapperPage } from '../../components/wrapper_page'; +import { AllCases } from './components/all_cases'; +import { SpyRoute } from '../../utils/route/spy_routes'; +import * as i18n from './translations'; +import { getCreateCaseUrl } from '../../components/link_to'; + +const badgeOptions = { + beta: true, + text: i18n.PAGE_BADGE_LABEL, + tooltip: i18n.PAGE_BADGE_TOOLTIP, +}; + +export const CasesPage = React.memo(() => ( + <> + + + + + {i18n.CREATE_TITLE} + + + + + + + > +)); + +CasesPage.displayName = 'CasesPage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx new file mode 100644 index 0000000000000..890df91c8560e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useParams } from 'react-router-dom'; + +import { CaseView } from './components/case_view'; +import { SpyRoute } from '../../utils/route/spy_routes'; + +export const CaseDetailsPage = React.memo(() => { + const { detailName: caseId } = useParams(); + if (!caseId) { + return null; + } + return ( + <> + + + > + ); +}); + +CaseDetailsPage.displayName = 'CaseDetailsPage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx new file mode 100644 index 0000000000000..92cd16fd2000e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiBadge, EuiTableFieldDataColumnType, EuiTableComputedColumnType } from '@elastic/eui'; +import { getEmptyTagValue } from '../../../../components/empty_value'; +import { Case } from '../../../../containers/case/types'; +import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; +import { CaseDetailsLink } from '../../../../components/links'; +import { TruncatableText } from '../../../../components/truncatable_text'; +import * as i18n from './translations'; + +export type CasesColumns = EuiTableFieldDataColumnType | EuiTableComputedColumnType; + +const renderStringField = (field: string) => (field != null ? field : getEmptyTagValue()); + +export const getCasesColumns = (): CasesColumns[] => [ + { + name: i18n.CASE_TITLE, + render: (theCase: Case) => { + if (theCase.case_id != null && theCase.title != null) { + return {theCase.title}; + } + return getEmptyTagValue(); + }, + }, + { + field: 'tags', + name: i18n.TAGS, + render: (tags: Case['tags']) => { + if (tags != null && tags.length > 0) { + return ( + + {tags.map((tag: string, i: number) => ( + + {tag} + + ))} + + ); + } + return getEmptyTagValue(); + }, + truncateText: true, + }, + { + field: 'created_at', + name: i18n.CREATED_AT, + sortable: true, + render: (createdAt: Case['created_at']) => { + if (createdAt != null) { + return ; + } + return getEmptyTagValue(); + }, + }, + { + field: 'created_by.username', + name: i18n.REPORTER, + render: (createdBy: Case['created_by']['username']) => renderStringField(createdBy), + }, + { + field: 'updated_at', + name: i18n.LAST_UPDATED, + sortable: true, + render: (updatedAt: Case['updated_at']) => { + if (updatedAt != null) { + return ; + } + return getEmptyTagValue(); + }, + }, + { + field: 'state', + name: i18n.STATE, + sortable: true, + render: (state: Case['state']) => renderStringField(state), + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx new file mode 100644 index 0000000000000..b1dd39c95e191 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo } from 'react'; +import { + EuiBasicTable, + EuiButton, + EuiEmptyPrompt, + EuiLoadingContent, + EuiTableSortingType, +} from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import * as i18n from './translations'; + +import { getCasesColumns } from './columns'; +import { SortFieldCase, Case, FilterOptions } from '../../../../containers/case/types'; + +import { Direction } from '../../../../graphql/types'; +import { useGetCases } from '../../../../containers/case/use_get_cases'; +import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; +import { Panel } from '../../../../components/panel'; +import { HeaderSection } from '../../../../components/header_section'; +import { CasesTableFilters } from './table_filters'; + +import { + UtilityBar, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../components/detection_engine/utility_bar'; +import { getCreateCaseUrl } from '../../../../components/link_to'; + +export const AllCases = React.memo(() => { + const [ + { data, isLoading, queryParams, filterOptions }, + setQueryParams, + setFilters, + ] = useGetCases(); + + const tableOnChangeCallback = useCallback( + ({ page, sort }: EuiBasicTableOnChange) => { + let newQueryParams = queryParams; + if (sort) { + let newSort; + switch (sort.field) { + case 'state': + newSort = SortFieldCase.state; + break; + case 'created_at': + newSort = SortFieldCase.createdAt; + break; + case 'updated_at': + newSort = SortFieldCase.updatedAt; + break; + default: + newSort = SortFieldCase.createdAt; + } + newQueryParams = { + ...newQueryParams, + sortField: newSort, + sortOrder: sort.direction as Direction, + }; + } + if (page) { + newQueryParams = { + ...newQueryParams, + page: page.index + 1, + perPage: page.size, + }; + } + setQueryParams(newQueryParams); + }, + [setQueryParams, queryParams] + ); + + const onFilterChangedCallback = useCallback( + (newFilterOptions: Partial) => { + setFilters({ ...filterOptions, ...newFilterOptions }); + }, + [filterOptions, setFilters] + ); + + const memoizedGetCasesColumns = useMemo(() => getCasesColumns(), []); + const memoizedPagination = useMemo( + () => ({ + pageIndex: queryParams.page - 1, + pageSize: queryParams.perPage, + totalItemCount: data.total, + pageSizeOptions: [5, 10, 20, 50, 100, 200, 300], + }), + [data, queryParams] + ); + + const sorting: EuiTableSortingType = { + sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, + }; + + return ( + + + + + {isLoading && isEmpty(data.cases) && ( + + )} + {!isLoading && !isEmpty(data.cases) && ( + <> + + + + {i18n.SHOWING_CASES(data.total ?? 0)} + + + + {i18n.NO_CASES}} + titleSize="xs" + body={i18n.NO_CASES_BODY} + actions={ + + {i18n.ADD_NEW_CASE} + + } + /> + } + onChange={tableOnChangeCallback} + pagination={memoizedPagination} + sorting={sorting} + /> + > + )} + + ); +}); + +AllCases.displayName = 'AllCases'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx new file mode 100644 index 0000000000000..e593623788046 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useState } from 'react'; +import { isEqual } from 'lodash/fp'; +import { EuiFieldSearch, EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import * as i18n from './translations'; + +import { FilterOptions } from '../../../../containers/case/types'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; +import { TagsFilterPopover } from '../../../../pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover'; + +interface Initial { + search: string; + tags: string[]; +} +interface CasesTableFiltersProps { + onFilterChanged: (filterOptions: Partial) => void; + initial: Initial; +} + +/** + * Collection of filters for filtering data within the CasesTable. Contains search bar, + * and tag selection + * + * @param onFilterChanged change listener to be notified on filter changes + */ + +const CasesTableFiltersComponent = ({ + onFilterChanged, + initial = { search: '', tags: [] }, +}: CasesTableFiltersProps) => { + const [search, setSearch] = useState(initial.search); + const [selectedTags, setSelectedTags] = useState(initial.tags); + const [{ isLoading, data }] = useGetTags(); + + const handleSelectedTags = useCallback( + newTags => { + if (!isEqual(newTags, selectedTags)) { + setSelectedTags(newTags); + onFilterChanged({ search, tags: newTags }); + } + }, + [search, selectedTags] + ); + const handleOnSearch = useCallback( + newSearch => { + const trimSearch = newSearch.trim(); + if (!isEqual(trimSearch, search)) { + setSearch(trimSearch); + onFilterChanged({ tags: selectedTags, search: trimSearch }); + } + }, + [search, selectedTags] + ); + + return ( + + + + + + + + + + + + ); +}; + +CasesTableFiltersComponent.displayName = 'CasesTableFiltersComponent'; + +export const CasesTableFilters = React.memo(CasesTableFiltersComponent); + +CasesTableFilters.displayName = 'CasesTableFilters'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts new file mode 100644 index 0000000000000..ab8e22ebcf1be --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../../translations'; + +export const ALL_CASES = i18n.translate('xpack.siem.case.caseTable.title', { + defaultMessage: 'All Cases', +}); +export const NO_CASES = i18n.translate('xpack.siem.case.caseTable.noCases.title', { + defaultMessage: 'No Cases', +}); +export const NO_CASES_BODY = i18n.translate('xpack.siem.case.caseTable.noCases.body', { + defaultMessage: 'Create a new case to see it displayed in the case workflow table.', +}); +export const ADD_NEW_CASE = i18n.translate('xpack.siem.case.caseTable.addNewCase', { + defaultMessage: 'Add New Case', +}); + +export const SHOWING_CASES = (totalRules: number) => + i18n.translate('xpack.siem.case.caseTable.showingCasesTitle', { + values: { totalRules }, + defaultMessage: 'Showing {totalRules} {totalRules, plural, =1 {case} other {cases}}', + }); + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.siem.case.caseTable.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {case} other {cases}}`, + }); + +export const SEARCH_CASES = i18n.translate( + 'xpack.siem.detectionEngine.case.caseTable.searchAriaLabel', + { + defaultMessage: 'Search cases', + } +); + +export const SEARCH_PLACEHOLDER = i18n.translate( + 'xpack.siem.detectionEngine.case.caseTable.searchPlaceholder', + { + defaultMessage: 'e.g. case name', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx new file mode 100644 index 0000000000000..4f43a6edeeac6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -0,0 +1,312 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, useEffect, useState } from 'react'; +import { + EuiBadge, + EuiButton, + EuiButtonEmpty, + EuiButtonToggle, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, +} from '@elastic/eui'; + +import styled, { css } from 'styled-components'; +import * as i18n from './translations'; +import { DescriptionMarkdown } from '../description_md_editor'; +import { Case } from '../../../../containers/case/types'; +import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; +import { getCaseUrl } from '../../../../components/link_to'; +import { HeaderPage } from '../../../../components/header_page_new'; +import { Markdown } from '../../../../components/markdown'; +import { PropertyActions } from '../property_actions'; +import { TagList } from '../tag_list'; +import { useGetCase } from '../../../../containers/case/use_get_case'; +import { UserActionTree } from '../user_action_tree'; +import { UserList } from '../user_list'; +import { useUpdateCase } from '../../../../containers/case/use_update_case'; +import { WrapperPage } from '../../../../components/wrapper_page'; + +interface Props { + caseId: string; +} + +const MyDescriptionList = styled(EuiDescriptionList)` + ${({ theme }) => css` + & { + padding-right: ${theme.eui.euiSizeL}; + border-right: ${theme.eui.euiBorderThin}; + } + `} +`; + +const MyWrapper = styled(WrapperPage)` + padding-bottom: 0; +`; +const BackgroundWrapper = styled.div` + ${({ theme }) => css` + background-color: ${theme.eui.euiColorEmptyShade}; + border-top: ${theme.eui.euiBorderThin}; + height: 100%; + `} +`; + +interface CasesProps { + caseId: string; + initialData: Case; + isLoading: boolean; +} + +export const Cases = React.memo(({ caseId, initialData, isLoading }) => { + const [{ data }, dispatchUpdateCaseProperty] = useUpdateCase(caseId, initialData); + const [isEditDescription, setIsEditDescription] = useState(false); + const [isEditTitle, setIsEditTitle] = useState(false); + const [isEditTags, setIsEditTags] = useState(false); + const [isCaseOpen, setIsCaseOpen] = useState(data.state === 'open'); + const [description, setDescription] = useState(data.description); + const [title, setTitle] = useState(data.title); + const [tags, setTags] = useState(data.tags); + + const onUpdateField = useCallback( + async (updateKey: keyof Case, updateValue: string | string[]) => { + switch (updateKey) { + case 'title': + if (updateValue.length > 0) { + dispatchUpdateCaseProperty({ + updateKey: 'title', + updateValue, + }); + setIsEditTitle(false); + } + break; + case 'description': + if (updateValue.length > 0) { + dispatchUpdateCaseProperty({ + updateKey: 'description', + updateValue, + }); + setIsEditDescription(false); + } + break; + case 'tags': + setTags(updateValue as string[]); + if (updateValue.length > 0) { + dispatchUpdateCaseProperty({ + updateKey: 'tags', + updateValue, + }); + setIsEditTags(false); + } + break; + default: + return null; + } + }, + [dispatchUpdateCaseProperty, title] + ); + + const onSetIsCaseOpen = useCallback(() => setIsCaseOpen(!isCaseOpen), [ + isCaseOpen, + setIsCaseOpen, + ]); + + useEffect(() => { + const caseState = isCaseOpen ? 'open' : 'closed'; + if (data.state !== caseState) { + dispatchUpdateCaseProperty({ + updateKey: 'state', + updateValue: caseState, + }); + } + }, [isCaseOpen]); + + // TO DO refactor each of these const's into their own components + const propertyActions = [ + { + iconType: 'documentEdit', + label: 'Edit description', + onClick: () => setIsEditDescription(true), + }, + { + iconType: 'securitySignalResolved', + label: 'Close case', + onClick: () => null, + }, + { + iconType: 'trash', + label: 'Delete case', + onClick: () => null, + }, + { + iconType: 'importAction', + label: 'Push as ServiceNow incident', + onClick: () => null, + }, + { + iconType: 'popout', + label: 'View ServiceNow incident', + onClick: () => null, + }, + { + iconType: 'importAction', + label: 'Update ServiceNow incident', + onClick: () => null, + }, + ]; + const userActions = [ + { + avatarName: data.created_by.username, + title: ( + + + + {`${data.created_by.username}`} + {` ${i18n.ADDED_DESCRIPTION} `}{' '} + + {/* STEPH FIX come back and add label `on` */} + + + + + + + ), + children: isEditDescription ? ( + <> + setDescription(updatedDescription)} + /> + + + + onUpdateField('description', description)} + > + {i18n.SUBMIT} + + + + setIsEditDescription(false)}> + {i18n.CANCEL} + + + + > + ) : ( + + ), + }, + ]; + return ( + <> + + setTitle(newTitle), + onSubmit: () => onUpdateField('title', title), + onClick: isEdit => setIsEditTitle(isEdit), + }} + isEditTitle={isEditTitle} + title={title} + > + + + + + + {i18n.STATUS} + + {data.state} + + + + {i18n.CASE_OPENED} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + onUpdateField('tags', newTags), + onClick: isEdit => setIsEditTags(isEdit), + }} + isEditTags={isEditTags} + /> + + + + + > + ); +}); + +export const CaseView = React.memo(({ caseId }: Props) => { + const [{ data, isLoading, isError }] = useGetCase(caseId); + if (isError) { + return null; + } + if (isLoading) { + return ( + + + + + + ); + } + + return ; +}); + +CaseView.displayName = 'CaseView'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts new file mode 100644 index 0000000000000..f45c52533d2e7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../../translations'; + +export const SHOWING_CASES = (actionDate: string, actionName: string, userName: string) => + i18n.translate('xpack.siem.case.caseView.actionHeadline', { + values: { + actionDate, + actionName, + userName, + }, + defaultMessage: '{userName} {actionName} on {actionDate}', + }); + +export const ADDED_DESCRIPTION = i18n.translate( + 'xpack.siem.case.caseView.actionLabel.addDescription', + { + defaultMessage: 'added description', + } +); + +export const EDITED_DESCRIPTION = i18n.translate( + 'xpack.siem.case.caseView.actionLabel.editDescription', + { + defaultMessage: 'edited description', + } +); + +export const ADDED_COMMENT = i18n.translate('xpack.siem.case.caseView.actionLabel.addComment', { + defaultMessage: 'added comment', +}); + +export const STATUS = i18n.translate('xpack.siem.case.caseView.statusLabel', { + defaultMessage: 'Status', +}); + +export const CASE_OPENED = i18n.translate('xpack.siem.case.caseView.caseOpened', { + defaultMessage: 'Case opened', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/form_options.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/create/form_options.ts new file mode 100644 index 0000000000000..7bc43e23a72c5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/form_options.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. + */ + +export const stateOptions = [ + { + value: 'open', + inputDisplay: 'Open', + }, + { + value: 'closed', + inputDisplay: 'Closed', + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx new file mode 100644 index 0000000000000..9fd1525003b0b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiLoadingSpinner, + EuiPanel, +} from '@elastic/eui'; +import styled from 'styled-components'; +import { Redirect } from 'react-router-dom'; +import { Field, Form, getUseField, useForm } from '../../../shared_imports'; +import { NewCase } from '../../../../containers/case/types'; +import { usePostCase } from '../../../../containers/case/use_post_case'; +import { schema } from './schema'; +import * as i18n from '../../translations'; +import { SiemPageName } from '../../../home/types'; +import { DescriptionMarkdown } from '../description_md_editor'; + +export const CommonUseField = getUseField({ component: Field }); + +const TagContainer = styled.div` + margin-top: 16px; +`; +const MySpinner = styled(EuiLoadingSpinner)` + position: absolute; + top: 50%; + left: 50%; +`; + +export const Create = React.memo(() => { + const [{ data, isLoading, newCase }, setFormData] = usePostCase(); + const { form } = useForm({ + defaultValue: data, + options: { stripEmptyFields: false }, + schema, + }); + + const onSubmit = useCallback(async () => { + const { isValid, data: newData } = await form.submit(); + if (isValid) { + setFormData({ ...newData, isNew: true } as NewCase); + } + }, [form]); + + if (newCase && newCase.case_id) { + return ; + } + return ( + + {isLoading && } + + + setFormData({ ...data, description })} + /> + + + + + <> + + + + + {i18n.SUBMIT} + + + + > + + ); +}); + +Create.displayName = 'Create'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx new file mode 100644 index 0000000000000..b86198e09ceac --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiText } from '@elastic/eui'; +import React from 'react'; + +import * as i18n from '../../../translations'; + +export const OptionalFieldLabel = ( + + {i18n.OPTIONAL} + +); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx new file mode 100644 index 0000000000000..1b5df72a6671c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx @@ -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. + */ + +import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../shared_imports'; +import { OptionalFieldLabel } from './optional_field_label'; +import * as i18n from '../../translations'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + title: { + type: FIELD_TYPES.TEXT, + label: i18n.CASE_TITLE, + validations: [ + { + validator: emptyField(i18n.TITLE_REQUIRED), + }, + ], + }, + description: { + type: FIELD_TYPES.TEXTAREA, + validations: [ + { + validator: emptyField(i18n.DESCRIPTION_REQUIRED), + }, + ], + }, + tags: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.TAGS, + helpText: i18n.TAGS_HELP, + labelAppend: OptionalFieldLabel, + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx new file mode 100644 index 0000000000000..44062a5a1d589 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { EuiFlexItem, EuiPanel, EuiTabbedContent, EuiTextArea } from '@elastic/eui'; +import React, { useState } from 'react'; +import styled from 'styled-components'; + +import { Markdown } from '../../../../components/markdown'; +import * as i18n from '../../translations'; +import { MarkdownHint } from '../../../../components/markdown/markdown_hint'; +import { CommonUseField } from '../create'; + +const TextArea = styled(EuiTextArea)<{ height: number }>` + min-height: ${({ height }) => `${height}px`}; + width: 100%; +`; + +TextArea.displayName = 'TextArea'; + +const DescriptionContainer = styled.div` + margin-top: 15px; + margin-bottom: 15px; +`; + +const DescriptionMarkdownTabs = styled(EuiTabbedContent)` + width: 100%; +`; + +DescriptionMarkdownTabs.displayName = 'DescriptionMarkdownTabs'; + +const MarkdownContainer = styled(EuiPanel)<{ height: number }>` + height: ${({ height }) => height}px; + overflow: auto; +`; + +MarkdownContainer.displayName = 'MarkdownContainer'; + +/** An input for entering a new case description */ +export const DescriptionMarkdown = React.memo<{ + descriptionInputHeight: number; + initialDescription: string; + isLoading: boolean; + formHook?: boolean; + onChange: (description: string) => void; +}>(({ initialDescription, isLoading, descriptionInputHeight, onChange, formHook = false }) => { + const [description, setDescription] = useState(initialDescription); + const tabs = [ + { + id: 'description', + name: i18n.DESCRIPTION, + content: formHook ? ( + { + setDescription(e as string); + onChange(e as string); + }} + componentProps={{ + idAria: 'caseDescription', + 'data-test-subj': 'caseDescription', + isDisabled: isLoading, + spellcheck: false, + }} + /> + ) : ( + { + setDescription(e.target.value); + onChange(e.target.value); + }} + fullWidth={true} + height={descriptionInputHeight} + aria-label={i18n.DESCRIPTION} + disabled={isLoading} + spellCheck={false} + value={description} + /> + ), + }, + { + id: 'preview', + name: i18n.PREVIEW, + content: ( + + + + ), + }, + ]; + return ( + + + + 0} /> + + + ); +}); + +DescriptionMarkdown.displayName = 'DescriptionMarkdown'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/constants.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/constants.ts new file mode 100644 index 0000000000000..14e4b46eb83f0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/constants.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const SET_STATE = 'SET_STATE'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx new file mode 100644 index 0000000000000..7fe5b6f5f8794 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPopover, EuiButtonIcon, EuiButtonEmpty } from '@elastic/eui'; + +export interface PropertyActionButtonProps { + onClick: () => void; + iconType: string; + label: string; +} + +const PropertyActionButton = React.memo( + ({ onClick, iconType, label }) => ( + + {label} + + ) +); + +PropertyActionButton.displayName = 'PropertyActionButton'; + +export interface PropertyActionsProps { + propertyActions: PropertyActionButtonProps[]; +} + +export const PropertyActions = React.memo(({ propertyActions }) => { + const [showActions, setShowActions] = useState(false); + + const onButtonClick = useCallback(() => { + setShowActions(!showActions); + }, [showActions]); + + const onClosePopover = useCallback((cb?: () => void) => { + setShowActions(false); + if (cb) { + cb(); + } + }, []); + + return ( + + + + } + id="settingsPopover" + isOpen={showActions} + closePopover={onClosePopover} + > + + {propertyActions.map((action, key) => ( + + onClosePopover(action.onClick)} + /> + + ))} + + + + + ); +}); + +PropertyActions.displayName = 'PropertyActions'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx new file mode 100644 index 0000000000000..6634672cb6a77 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { + EuiText, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, +} from '@elastic/eui'; +import styled, { css } from 'styled-components'; +import * as i18n from '../../translations'; +import { Form, useForm } from '../../../shared_imports'; +import { schema } from './schema'; +import { CommonUseField } from '../create'; + +interface IconAction { + 'aria-label': string; + iconType: string; + onClick: (b: boolean) => void; + onSubmit: (a: string[]) => void; +} + +interface TagListProps { + tags: string[]; + iconAction?: IconAction; + isEditTags?: boolean; +} + +const MyFlexGroup = styled(EuiFlexGroup)` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSizeM}; + p { + font-size: ${theme.eui.euiSizeM}; + } + `} +`; + +export const TagList = React.memo(({ tags, isEditTags, iconAction }: TagListProps) => { + const { form } = useForm({ + defaultValue: { tags }, + options: { stripEmptyFields: false }, + schema, + }); + + const onSubmit = useCallback(async () => { + const { isValid, data: newData } = await form.submit(); + if (isValid && iconAction) { + iconAction.onSubmit(newData.tags); + iconAction.onClick(false); + } + }, [form]); + + const onActionClick = useCallback( + (cb: (b: boolean) => void, onClickBool: boolean) => cb(onClickBool), + [iconAction] + ); + return ( + + + + {i18n.TAGS} + + {iconAction && ( + + onActionClick(iconAction.onClick, true)} + /> + + )} + + + + {tags.length === 0 && !isEditTags && {i18n.NO_TAGS}} + {tags.length > 0 && + !isEditTags && + tags.map((tag, key) => ( + + {tag} + + ))} + {isEditTags && iconAction && ( + + + + + + + + + {i18n.SUBMIT} + + + + onActionClick(iconAction.onClick, false)}> + {i18n.CANCEL} + + + + )} + + + ); +}); + +TagList.displayName = 'TagList'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/schema.tsx new file mode 100644 index 0000000000000..dfc9c61cd5f0c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/schema.tsx @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FormSchema } from '../../../shared_imports'; +import { schema as createSchema } from '../create/schema'; + +export const schema: FormSchema = { + tags: createSchema.tags, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx new file mode 100644 index 0000000000000..8df98a4cef0e8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ReactNode } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiAvatar, EuiPanel, EuiText } from '@elastic/eui'; +import styled, { css } from 'styled-components'; + +export interface UserActionItem { + avatarName: string; + children?: ReactNode; + title: ReactNode; +} + +export interface UserActionTreeProps { + userActions: UserActionItem[]; +} + +const UserAction = styled(EuiFlexGroup)` + ${({ theme }) => css` + & { + background-image: linear-gradient( + to right, + transparent 0, + transparent 15px, + ${theme.eui.euiBorderColor} 15px, + ${theme.eui.euiBorderColor} 17px, + transparent 17px, + transparent 100% + ); + background-repeat: no-repeat; + background-position: left ${theme.eui.euiSizeXXL}; + margin-bottom: ${theme.eui.euiSizeS}; + } + .userAction__panel { + margin-bottom: ${theme.eui.euiSize}; + } + .userAction__circle { + flex-shrink: 0; + margin-right: ${theme.eui.euiSize}; + vertical-align: top; + } + .userAction__title { + padding: ${theme.eui.euiSizeS} ${theme.eui.euiSizeL}; + background: ${theme.eui.euiColorLightestShade}; + border-bottom: ${theme.eui.euiBorderThin}; + border-radius: ${theme.eui.euiBorderRadius} ${theme.eui.euiBorderRadius} 0 0; + } + .userAction__content { + padding: ${theme.eui.euiSizeM} ${theme.eui.euiSizeL}; + } + .euiText--small * { + margin-bottom: 0; + } + `} +`; + +const renderUserActions = (userActions: UserActionItem[]) => { + return userActions.map(({ avatarName, children, title }, key) => ( + + + + + + + + {title} + + {children && {children}} + + + + )); +}; + +export const UserActionTree = React.memo(({ userActions }: UserActionTreeProps) => ( + {renderUserActions(userActions)} +)); + +UserActionTree.displayName = 'UserActionTree'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx new file mode 100644 index 0000000000000..b80ee58f8abbf --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + EuiButtonIcon, + EuiText, + EuiHorizontalRule, + EuiAvatar, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import styled, { css } from 'styled-components'; +import { ElasticUser } from '../../../../containers/case/types'; + +interface UserListProps { + headline: string; + users: ElasticUser[]; +} + +const MyAvatar = styled(EuiAvatar)` + top: -4px; +`; + +const MyFlexGroup = styled(EuiFlexGroup)` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSizeM}; + `} +`; + +const renderUsers = (users: ElasticUser[]) => { + return users.map(({ username }, key) => ( + + + + + + + + + + {username} + + + + + + + window.alert('Email clicked')} + iconType="email" + aria-label="email" + /> + + + )); +}; + +export const UserList = React.memo(({ headline, users }: UserListProps) => { + return ( + + {headline} + + {renderUsers(users)} + + ); +}); + +UserList.displayName = 'UserList'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx new file mode 100644 index 0000000000000..9bc356517cc68 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { WrapperPage } from '../../components/wrapper_page'; +import { Create } from './components/create'; +import { SpyRoute } from '../../utils/route/spy_routes'; +import { HeaderPage } from '../../components/header_page'; +import * as i18n from './translations'; +import { getCaseUrl } from '../../components/link_to'; + +const backOptions = { + href: getCaseUrl(), + text: i18n.BACK_TO_ALL, +}; +const badgeOptions = { + beta: true, + text: i18n.PAGE_BADGE_LABEL, + tooltip: i18n.PAGE_BADGE_TOOLTIP, +}; +export const CreateCasePage = React.memo(() => ( + <> + + + + + + > +)); + +CreateCasePage.displayName = 'CreateCasePage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx new file mode 100644 index 0000000000000..9bd91b1c6d62d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { Route, Switch } from 'react-router-dom'; +import { SiemPageName } from '../home/types'; +import { CaseDetailsPage } from './case_details'; +import { CasesPage } from './case'; +import { CreateCasePage } from './create_case'; + +const casesPagePath = `/:pageName(${SiemPageName.case})`; +const caseDetailsPagePath = `${casesPagePath}/:detailName`; +const createCasePagePath = `${casesPagePath}/create`; + +const CaseContainerComponent: React.FC = () => ( + + + + + + + + + + + +); + +export const Case = React.memo(CaseContainerComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts new file mode 100644 index 0000000000000..4e878ba58411e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const BACK_TO_ALL = i18n.translate('xpack.siem.case.caseView.backLabel', { + defaultMessage: 'Back to cases', +}); + +export const CANCEL = i18n.translate('xpack.siem.case.caseView.cancel', { + defaultMessage: 'Cancel', +}); + +export const CASE_TITLE = i18n.translate('xpack.siem.case.caseView.caseTitle', { + defaultMessage: 'Case Title', +}); + +export const CREATED_AT = i18n.translate('xpack.siem.case.caseView.createdAt', { + defaultMessage: 'Created at', +}); + +export const REPORTER = i18n.translate('xpack.siem.case.caseView.createdBy', { + defaultMessage: 'Reporter', +}); + +export const CREATE_BC_TITLE = i18n.translate('xpack.siem.case.caseView.breadcrumb', { + defaultMessage: 'Create', +}); + +export const CREATE_TITLE = i18n.translate('xpack.siem.case.caseView.create', { + defaultMessage: 'Create new case', +}); + +export const DESCRIPTION = i18n.translate('xpack.siem.case.caseView.description', { + defaultMessage: 'Description', +}); + +export const DESCRIPTION_REQUIRED = i18n.translate( + 'xpack.siem.case.createCase.descriptionFieldRequiredError', + { + defaultMessage: 'A description is required.', + } +); + +export const EDIT = i18n.translate('xpack.siem.case.caseView.edit', { + defaultMessage: 'Edit', +}); + +export const OPTIONAL = i18n.translate('xpack.siem.case.caseView.optional', { + defaultMessage: 'Optional', +}); + +export const LAST_UPDATED = i18n.translate('xpack.siem.case.caseView.updatedAt', { + defaultMessage: 'Last updated', +}); + +export const PAGE_BADGE_LABEL = i18n.translate('xpack.siem.case.caseView.pageBadgeLabel', { + defaultMessage: 'Beta', +}); + +export const PAGE_BADGE_TOOLTIP = i18n.translate('xpack.siem.case.caseView.pageBadgeTooltip', { + defaultMessage: + 'Case Workflow is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', +}); + +export const PAGE_SUBTITLE = i18n.translate('xpack.siem.case.caseView.pageSubtitle', { + defaultMessage: 'Case Workflow Management within the Elastic SIEM', +}); + +export const PAGE_TITLE = i18n.translate('xpack.siem.case.pageTitle', { + defaultMessage: 'Case Workflows', +}); + +export const PREVIEW = i18n.translate('xpack.siem.case.caseView.preview', { + defaultMessage: 'Preview', +}); + +export const STATE = i18n.translate('xpack.siem.case.caseView.state', { + defaultMessage: 'State', +}); + +export const SUBMIT = i18n.translate('xpack.siem.case.caseView.submit', { + defaultMessage: 'Submit', +}); + +export const TAGS = i18n.translate('xpack.siem.case.caseView.tags', { + defaultMessage: 'Tags', +}); + +export const TAGS_HELP = i18n.translate('xpack.siem.case.createCase.fieldTagsHelpText', { + defaultMessage: + 'Type one or more custom identifying tags for this case. Press enter after each tag to begin a new one.', +}); + +export const NO_TAGS = i18n.translate('xpack.siem.case.caseView.noTags', { + defaultMessage: 'No tags are currently assigned to this case.', +}); + +export const TITLE_REQUIRED = i18n.translate('xpack.siem.case.createCase.titleFieldRequiredError', { + defaultMessage: 'A title is required.', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/utils.ts b/x-pack/legacy/plugins/siem/public/pages/case/utils.ts new file mode 100644 index 0000000000000..bd6cb5da5eb01 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/utils.ts @@ -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. + */ + +import { Breadcrumb } from 'ui/chrome'; +import { getCaseDetailsUrl, getCaseUrl, getCreateCaseUrl } from '../../components/link_to'; +import { RouteSpyState } from '../../utils/route/types'; +import * as i18n from './translations'; + +export const getBreadcrumbs = (params: RouteSpyState): Breadcrumb[] => { + let breadcrumb = [ + { + text: i18n.PAGE_TITLE, + href: getCaseUrl(), + }, + ]; + if (params.detailName === 'create') { + breadcrumb = [ + ...breadcrumb, + { + text: i18n.CREATE_BC_TITLE, + href: getCreateCaseUrl(), + }, + ]; + } else if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: params.detailName, + href: getCaseDetailsUrl(params.detailName), + }, + ]; + } + return breadcrumb; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx index c54a2e8d49844..fa4f6a874ca5e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx @@ -13,6 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; +import { isEqual } from 'lodash/fp'; import * as i18n from '../../translations'; import { FilterOptions } from '../../../../../containers/detection_engine/rules'; @@ -59,6 +60,15 @@ const RulesTableFiltersComponent = ({ setShowElasticRules(false); }, [setShowElasticRules, showCustomRules, setShowCustomRules]); + const handleSelectedTags = useCallback( + newTags => { + if (!isEqual(newTags, selectedTags)) { + setSelectedTags(newTags); + } + }, + [selectedTags] + ); + return ( @@ -74,9 +84,10 @@ const RulesTableFiltersComponent = ({ diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx index b9d2c97f063b1..44149a072f5c1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import React, { Dispatch, SetStateAction, useState } from 'react'; import { EuiFilterButton, EuiFilterSelectItem, @@ -19,9 +19,10 @@ import * as i18n from '../../translations'; import { toggleSelectedGroup } from '../../../../../components/ml_popover/jobs_table/filters/toggle_selected_group'; interface TagsFilterPopoverProps { + selectedTags: string[]; tags: string[]; onSelectedTagsChanged: Dispatch>; - isLoading: boolean; + isLoading: boolean; // TO DO reimplement? } const ScrollableDiv = styled.div` @@ -37,14 +38,10 @@ const ScrollableDiv = styled.div` */ export const TagsFilterPopoverComponent = ({ tags, + selectedTags, onSelectedTagsChanged, }: TagsFilterPopoverProps) => { const [isTagPopoverOpen, setIsTagPopoverOpen] = useState(false); - const [selectedTags, setSelectedTags] = useState([]); - - useEffect(() => { - onSelectedTagsChanged(selectedTags); - }, [selectedTags.sort().join()]); return ( toggleSelectedGroup(tag, selectedTags, setSelectedTags)} + onClick={() => toggleSelectedGroup(tag, selectedTags, onSelectedTagsChanged)} > {`${tag}`} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx index 0c75da7d8a632..cc5e9b38eb2f8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx @@ -18,7 +18,7 @@ import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 're import styled from 'styled-components'; import * as RuleI18n from '../../translations'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; interface AddItemProps { addText: string; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx index 09f4c13acbf69..1cc7bba5558db 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx @@ -19,7 +19,7 @@ import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/searc import { useKibana } from '../../../../../lib/kibana'; import { IMitreEnterpriseAttack } from '../../types'; import { FieldValueTimeline } from '../pick_timeline'; -import { FormSchema } from '../shared_imports'; +import { FormSchema } from '../../../../shared_imports'; import { ListItems } from './types'; import { buildQueryBarDescription, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx index d85be053065fc..b49126c8c0fe0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx @@ -20,7 +20,7 @@ import styled from 'styled-components'; import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; import * as Rulei18n from '../../translations'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; import { threatDefault } from '../step_about_rule/default_value'; import { IMitreEnterpriseAttack } from '../../types'; import { MyAddItemButton } from '../add_item_form'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx index f467d0ebede41..56cb02c9ec817 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx @@ -8,7 +8,7 @@ import { EuiFormRow } from '@elastic/eui'; import React, { useCallback, useEffect, useState } from 'react'; import { SearchTimelineSuperSelect } from '../../../../../components/timeline/search_super_select'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; export interface FieldValueTimeline { id: string | null; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx index 7f55d76c6d6b1..88795f9195e68 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx @@ -29,7 +29,7 @@ import { convertKueryToElasticSearchQuery } from '../../../../../lib/keury'; import { useKibana } from '../../../../../lib/kibana'; import { TimelineModel } from '../../../../../store/timeline/model'; import { useSavedQueryServices } from '../../../../../utils/saved_query_services'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; import * as i18n from './translations'; export interface FieldValueQueryBar { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx index 3bde2087f26b1..ffb6c4eda3243 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx @@ -16,7 +16,7 @@ import { isEmpty } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; import * as I18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx index 9c351e66c2f04..45da7d081333e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx @@ -23,7 +23,14 @@ import * as RuleI18n from '../../translations'; import { AddItem } from '../add_item_form'; import { StepRuleDescription } from '../description_step'; import { AddMitreThreat } from '../mitre'; -import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; +import { + Field, + Form, + FormDataProvider, + getUseField, + UseField, + useForm, +} from '../../../../shared_imports'; import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data'; import { stepAboutDefaultValue } from './default_value'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx index 22033dcf6b0f7..27887bcbbe600 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx @@ -13,7 +13,7 @@ import { FormSchema, ValidationFunc, ERROR_CODE, -} from '../shared_imports'; +} from '../../../../shared_imports'; import { isMitreAttackInvalid } from '../mitre/helpers'; import { OptionalFieldLabel } from '../optional_field_label'; import { isUrlInvalid } from './helpers'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index 5409a5f161bba..920a9f2dfe56c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -25,7 +25,14 @@ import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; import { StepRuleDescription } from '../description_step'; import { QueryBarDefineRule } from '../query_bar'; import { StepContentWrapper } from '../step_content_wrapper'; -import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; +import { + Field, + Form, + FormDataProvider, + getUseField, + UseField, + useForm, +} from '../../../../shared_imports'; import { schema } from './schema'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx index 079ec0dab4c5a..bb178d7197069 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx @@ -17,7 +17,7 @@ import { fieldValidators, FormSchema, ValidationFunc, -} from '../shared_imports'; +} from '../../../../shared_imports'; import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; const { emptyField } = fieldValidators; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx index 532df628a83ae..cfbb0a622c721 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx @@ -12,7 +12,7 @@ import { setFieldValue } from '../../helpers'; import { RuleStep, RuleStepProps, ScheduleStepRule } from '../../types'; import { StepRuleDescription } from '../description_step'; import { ScheduleItem } from '../schedule_item_form'; -import { Form, UseField, useForm } from '../shared_imports'; +import { Form, UseField, useForm } from '../../../../shared_imports'; import { StepContentWrapper } from '../step_content_wrapper'; import { schema } from './schema'; import * as I18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx index a951c1fab7cc8..9932e4f6ef435 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { OptionalFieldLabel } from '../optional_field_label'; -import { FormSchema } from '../shared_imports'; +import { FormSchema } from '../../../../shared_imports'; export const schema: FormSchema = { interval: { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index 3adc22329ac4f..c985045b1897b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -17,7 +17,7 @@ import { displaySuccessToast, useStateToaster } from '../../../../components/toa import { SpyRoute } from '../../../../utils/route/spy_routes'; import { useUserInfo } from '../../components/user_info'; import { AccordionTitle } from '../components/accordion_title'; -import { FormData, FormHook } from '../components/shared_imports'; +import { FormData, FormHook } from '../../../shared_imports'; import { StepAboutRule } from '../components/step_about_rule'; import { StepDefineRule } from '../components/step_define_rule'; import { StepScheduleRule } from '../components/step_schedule_rule'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index 99fcff6b8d2fd..0fac4641e54a7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -26,7 +26,7 @@ import { displaySuccessToast, useStateToaster } from '../../../../components/toa import { SpyRoute } from '../../../../utils/route/spy_routes'; import { useUserInfo } from '../../components/user_info'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; -import { FormHook, FormData } from '../components/shared_imports'; +import { FormHook, FormData } from '../../../shared_imports'; import { StepPanel } from '../components/step_panel'; import { StepAboutRule } from '../components/step_about_rule'; import { StepDefineRule } from '../components/step_define_rule'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index cfff71851b2e1..3fab456d856ca 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -11,7 +11,7 @@ import { useLocation } from 'react-router-dom'; import { Filter } from '../../../../../../../../src/plugins/data/public'; import { Rule } from '../../../containers/detection_engine/rules'; -import { FormData, FormHook, FormSchema } from './components/shared_imports'; +import { FormData, FormHook, FormSchema } from '../../shared_imports'; import { AboutStepRule, DefineStepRule, IMitreEnterpriseAttack, ScheduleStepRule } from './types'; interface GetStepsData { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index fc2e3fba24449..55eb45fb5ed9d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -7,7 +7,7 @@ import { Filter } from '../../../../../../../../src/plugins/data/common'; import { Rule } from '../../../containers/detection_engine/rules'; import { FieldValueQueryBar } from './components/query_bar'; -import { FormData, FormHook } from './components/shared_imports'; +import { FormData, FormHook } from '../../shared_imports'; import { FieldValueTimeline } from './components/pick_timeline'; export interface EuiBasicTableSortTypes { diff --git a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx index c0e959c5e97fa..42d333f4f893e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx @@ -10,6 +10,7 @@ import { getNetworkUrl, getTimelinesUrl, getHostsUrl, + getCaseUrl, } from '../../components/link_to'; import * as i18n from './translations'; import { SiemPageName, SiemNavTab } from './types'; @@ -50,4 +51,11 @@ export const navTabs: SiemNavTab = { disabled: false, urlKey: 'timeline', }, + [SiemPageName.case]: { + id: SiemPageName.case, + name: i18n.CASE, + href: getCaseUrl(), + disabled: true, + urlKey: 'case', + }, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx index 5cfed4121ba77..9ee103f88793d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx @@ -26,6 +26,7 @@ import { DetectionEngineContainer } from '../detection_engine'; import { HostsContainer } from '../hosts'; import { NetworkContainer } from '../network'; import { Overview } from '../overview'; +import { Case } from '../case'; import { Timelines } from '../timelines'; import { navTabs } from './home_navigations'; import { SiemPageName } from './types'; @@ -42,6 +43,11 @@ const WrappedByAutoSizer = styled.div` `; WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; +const Main = styled.main` + height: 100%; +`; +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 */ @@ -61,7 +67,7 @@ export const HomePage: React.FC = () => ( - + {({ browserFields, indexPattern, indicesExist }) => ( @@ -131,12 +137,15 @@ export const HomePage: React.FC = () => ( )} /> + + + } /> )} - + diff --git a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts index 80800a3bd4198..581c81d9f98a0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts @@ -25,3 +25,7 @@ export const DETECTION_ENGINE = i18n.translate('xpack.siem.navigation.detectionE export const TIMELINES = i18n.translate('xpack.siem.navigation.timelines', { defaultMessage: 'Timelines', }); + +export const CASE = i18n.translate('xpack.siem.navigation.case', { + defaultMessage: 'Case', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/home/types.ts b/x-pack/legacy/plugins/siem/public/pages/home/types.ts index 678de6dbcc128..6445ac91d9e13 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/home/types.ts @@ -12,6 +12,7 @@ export enum SiemPageName { network = 'network', detections = 'detections', timelines = 'timelines', + case = 'case', } export type SiemNavTabKey = @@ -19,6 +20,7 @@ export type SiemNavTabKey = | SiemPageName.hosts | SiemPageName.network | SiemPageName.detections - | SiemPageName.timelines; + | SiemPageName.timelines + | SiemPageName.case; export type SiemNavTab = Record; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/shared_imports.ts b/x-pack/legacy/plugins/siem/public/pages/shared_imports.ts similarity index 50% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/shared_imports.ts rename to x-pack/legacy/plugins/siem/public/pages/shared_imports.ts index 494da24be706a..a41f121b36926 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/shared_imports.ts +++ b/x-pack/legacy/plugins/siem/public/pages/shared_imports.ts @@ -17,7 +17,7 @@ export { UseField, useForm, ValidationFunc, -} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -export { Field } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; -export { fieldValidators } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; -export { ERROR_CODE } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; +} from '../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +export { Field } from '../../../../../../src/plugins/es_ui_shared/static/forms/components'; +export { fieldValidators } from '../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; +export { ERROR_CODE } from '../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; diff --git a/x-pack/legacy/plugins/siem/public/store/model.ts b/x-pack/legacy/plugins/siem/public/store/model.ts index 6f04f22866be5..9e9e663a59fe0 100644 --- a/x-pack/legacy/plugins/siem/public/store/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/model.ts @@ -5,9 +5,9 @@ */ export { appModel } from './app'; -export { inputsModel } from './inputs'; -export { hostsModel } from './hosts'; export { dragAndDropModel } from './drag_and_drop'; +export { hostsModel } from './hosts'; +export { inputsModel } from './inputs'; export { networkModel } from './network'; export type KueryFilterQueryKind = 'kuery' | 'lucene'; diff --git a/x-pack/legacy/plugins/siem/public/utils/use_global_loading.ts b/x-pack/legacy/plugins/siem/public/utils/use_global_loading.ts deleted file mode 100644 index 37abe2f28d310..0000000000000 --- a/x-pack/legacy/plugins/siem/public/utils/use_global_loading.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useState, useEffect } from 'react'; - -export const useGlobalLoading = () => { - const [isInitializing, setIsInitializing] = useState(true); - useEffect(() => { - if (isInitializing) { - setIsInitializing(false); - } - }); - return isInitializing; -}; diff --git a/x-pack/legacy/plugins/siem/server/index.ts b/x-pack/legacy/plugins/siem/server/index.ts index 882475390ae98..8513f871cb6c1 100644 --- a/x-pack/legacy/plugins/siem/server/index.ts +++ b/x-pack/legacy/plugins/siem/server/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext } from '../../../../../src/core/server'; import { Plugin } from './plugin'; export const plugin = (context: PluginInitializerContext) => { diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts deleted file mode 100644 index bab7936005c04..0000000000000 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ /dev/null @@ -1,86 +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 { PluginInitializerContext } from 'src/core/server'; - -import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type'; -import { createRulesRoute } from './lib/detection_engine/routes/rules/create_rules_route'; -import { createIndexRoute } from './lib/detection_engine/routes/index/create_index_route'; -import { readIndexRoute } from './lib/detection_engine/routes/index/read_index_route'; -import { readRulesRoute } from './lib/detection_engine/routes/rules/read_rules_route'; -import { findRulesRoute } from './lib/detection_engine/routes/rules/find_rules_route'; -import { deleteRulesRoute } from './lib/detection_engine/routes/rules/delete_rules_route'; -import { patchRulesRoute } from './lib/detection_engine/routes/rules/patch_rules_route'; -import { setSignalsStatusRoute } from './lib/detection_engine/routes/signals/open_close_signals_route'; -import { querySignalsRoute } from './lib/detection_engine/routes/signals/query_signals_route'; -import { ServerFacade } from './types'; -import { deleteIndexRoute } from './lib/detection_engine/routes/index/delete_index_route'; -import { isAlertExecutor } from './lib/detection_engine/signals/types'; -import { readTagsRoute } from './lib/detection_engine/routes/tags/read_tags_route'; -import { readPrivilegesRoute } from './lib/detection_engine/routes/privileges/read_privileges_route'; -import { addPrepackedRulesRoute } from './lib/detection_engine/routes/rules/add_prepackaged_rules_route'; -import { createRulesBulkRoute } from './lib/detection_engine/routes/rules/create_rules_bulk_route'; -import { patchRulesBulkRoute } from './lib/detection_engine/routes/rules/patch_rules_bulk_route'; -import { deleteRulesBulkRoute } from './lib/detection_engine/routes/rules/delete_rules_bulk_route'; -import { importRulesRoute } from './lib/detection_engine/routes/rules/import_rules_route'; -import { exportRulesRoute } from './lib/detection_engine/routes/rules/export_rules_route'; -import { findRulesStatusesRoute } from './lib/detection_engine/routes/rules/find_rules_status_route'; -import { getPrepackagedRulesStatusRoute } from './lib/detection_engine/routes/rules/get_prepackaged_rules_status_route'; -import { updateRulesRoute } from './lib/detection_engine/routes/rules/update_rules_route'; -import { updateRulesBulkRoute } from './lib/detection_engine/routes/rules/update_rules_bulk_route'; - -const APP_ID = 'siem'; - -export const initServerWithKibana = (context: PluginInitializerContext, __legacy: ServerFacade) => { - const logger = context.logger.get('plugins', APP_ID); - const version = context.env.packageInfo.version; - - if (__legacy.plugins.alerting != null) { - const type = signalRulesAlertType({ logger, version }); - if (isAlertExecutor(type)) { - __legacy.plugins.alerting.setup.registerType(type); - } - } - - // Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules - // All REST rule creation, deletion, updating, etc... - createRulesRoute(__legacy); - readRulesRoute(__legacy); - updateRulesRoute(__legacy); - deleteRulesRoute(__legacy); - findRulesRoute(__legacy); - patchRulesRoute(__legacy); - - addPrepackedRulesRoute(__legacy); - getPrepackagedRulesStatusRoute(__legacy); - createRulesBulkRoute(__legacy); - updateRulesBulkRoute(__legacy); - deleteRulesBulkRoute(__legacy); - patchRulesBulkRoute(__legacy); - - importRulesRoute(__legacy); - exportRulesRoute(__legacy); - - findRulesStatusesRoute(__legacy); - - // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals - // POST /api/detection_engine/signals/status - // Example usage can be found in siem/server/lib/detection_engine/scripts/signals - setSignalsStatusRoute(__legacy); - querySignalsRoute(__legacy); - - // Detection Engine index routes that have the REST endpoints of /api/detection_engine/index - // All REST index creation, policy management for spaces - createIndexRoute(__legacy); - readIndexRoute(__legacy); - deleteIndexRoute(__legacy); - - // Detection Engine tags routes that have the REST endpoints of /api/detection_engine/tags - readTagsRoute(__legacy); - - // Privileges API to get the generic user privileges - readPrivilegesRoute(__legacy); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts index 3aefb6c0e1e5f..210c97892e25c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts @@ -29,7 +29,6 @@ describe('alerts elasticsearch_adapter', () => { return mockAlertsHistogramDataResponse; }); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings_temp.ts b/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings.ts similarity index 77% rename from x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings_temp.ts rename to x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings.ts index bd73805600a33..80cdb9e979a68 100644 --- a/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings_temp.ts +++ b/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings.ts @@ -5,10 +5,7 @@ */ /* eslint-disable @typescript-eslint/no-empty-interface */ /* eslint-disable @typescript-eslint/camelcase */ -import { - NewCaseFormatted, - NewCommentFormatted, -} from '../../../../../../../x-pack/plugins/case/server'; +import { CaseAttributes, CommentAttributes } from '../../../../../../../x-pack/plugins/case/server'; import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings'; // Temporary file to write mappings for case @@ -19,20 +16,10 @@ export const caseSavedObjectType = 'case-workflow'; export const caseCommentSavedObjectType = 'case-workflow-comment'; export const caseSavedObjectMappings: { - [caseSavedObjectType]: ElasticsearchMappingOf; + [caseSavedObjectType]: ElasticsearchMappingOf; } = { [caseSavedObjectType]: { properties: { - assignees: { - properties: { - username: { - type: 'keyword', - }, - full_name: { - type: 'keyword', - }, - }, - }, created_at: { type: 'date', }, @@ -58,15 +45,15 @@ export const caseSavedObjectMappings: { tags: { type: 'keyword', }, - case_type: { - type: 'keyword', + updated_at: { + type: 'date', }, }, }, }; export const caseCommentSavedObjectMappings: { - [caseCommentSavedObjectType]: ElasticsearchMappingOf; + [caseCommentSavedObjectType]: ElasticsearchMappingOf; } = { [caseCommentSavedObjectType]: { properties: { @@ -86,6 +73,9 @@ export const caseCommentSavedObjectMappings: { }, }, }, + updated_at: { + type: 'date', + }, }, }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts index 30fdf7520a3ed..0ab6f1a8df779 100644 --- a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, PluginInitializerContext } from '../../../../../../../src/core/server'; -import { PluginsSetup } from '../../plugin'; +import { CoreSetup, SetupPlugins } from '../../plugin'; import { Anomalies } from '../anomalies'; import { ElasticsearchAnomaliesAdapter } from '../anomalies/elasticsearch_adapter'; @@ -37,10 +36,10 @@ import { Alerts, ElasticsearchAlertsAdapter } from '../alerts'; export function compose( core: CoreSetup, - plugins: PluginsSetup, - env: PluginInitializerContext['env'] + plugins: SetupPlugins, + isProductionMode: boolean ): AppBackendLibs { - const framework = new KibanaBackendFrameworkAdapter(core, plugins, env); + const framework = new KibanaBackendFrameworkAdapter(core, plugins, isProductionMode); const sources = new Sources(new ConfigurationSourcesAdapter()); const sourceStatus = new SourceStatus(new ElasticsearchSourceStatusAdapter(framework)); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts index dff6e7136bff2..253bccad2e9f8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts @@ -4,18 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; // See the reference(s) below on explanations about why -000001 was chosen and // why the is_write_index is true as well as the bootstrapping step which is needed. // Ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/applying-policy-to-template.html export const createBootstrapIndex = async ( - callWithRequest: CallWithRequest< - { path: string; method: 'PUT'; body: unknown }, - CallClusterOptions, - boolean - >, + callWithRequest: CallWithRequest<{ path: string; method: 'PUT'; body: unknown }, boolean>, index: string ): Promise => { return callWithRequest('transport.request', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts index b1d8f994615ae..d165bf69f1da1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts @@ -5,11 +5,10 @@ */ import { IndicesDeleteParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const deleteAllIndex = async ( - callWithRequest: CallWithRequest, + callWithRequest: CallWithRequest, index: string ): Promise => { return callWithRequest('indices.delete', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts index aa31c427ec84f..00213e271c7e8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts @@ -7,7 +7,7 @@ import { CallWithRequest } from '../types'; export const deletePolicy = async ( - callWithRequest: CallWithRequest<{ path: string; method: 'DELETE' }, {}, unknown>, + callWithRequest: CallWithRequest<{ path: string; method: 'DELETE' }, unknown>, policy: string ): Promise => { return callWithRequest('transport.request', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts index 63c32d13ccb8d..3402c25fb1ab1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts @@ -5,11 +5,10 @@ */ import { IndicesDeleteTemplateParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const deleteTemplate = async ( - callWithRequest: CallWithRequest, + callWithRequest: CallWithRequest, name: string ): Promise => { return callWithRequest('indices.deleteTemplate', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts index 705f542b50124..d81f23a283451 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts @@ -9,7 +9,6 @@ import { CallWithRequest } from '../types'; export const getIndexExists = async ( callWithRequest: CallWithRequest< { index: string; size: number; terminate_after: number; allow_no_indices: boolean }, - {}, { _shards: { total: number } } >, index: string diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts index d5ab1a10180c0..8a54ceac8ab78 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts @@ -7,7 +7,7 @@ import { CallWithRequest } from '../types'; export const getPolicyExists = async ( - callWithRequest: CallWithRequest<{ path: string; method: 'GET' }, {}, unknown>, + callWithRequest: CallWithRequest<{ path: string; method: 'GET' }, unknown>, policy: string ): Promise => { try { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts index fac402155619e..fd5eec8db4140 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts @@ -5,11 +5,10 @@ */ import { IndicesExistsTemplateParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const getTemplateExists = async ( - callWithRequest: CallWithRequest, + callWithRequest: CallWithRequest, template: string ): Promise => { return callWithRequest('indices.existsTemplate', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts index 0abe2b992b780..ca987f85c446c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts @@ -5,11 +5,10 @@ */ import { IndicesGetSettingsParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const readIndex = async ( - callWithRequest: CallWithRequest, + callWithRequest: CallWithRequest, index: string ): Promise => { return callWithRequest('indices.get', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts index fae28bab749ca..90d5bf9a9871b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts @@ -7,7 +7,7 @@ import { CallWithRequest } from '../types'; export const setPolicy = async ( - callWithRequest: CallWithRequest<{ path: string; method: 'PUT'; body: unknown }, {}, unknown>, + callWithRequest: CallWithRequest<{ path: string; method: 'PUT'; body: unknown }, unknown>, policy: string, body: unknown ): Promise => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts index dc9ad5dda9f7d..0894f930feffb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts @@ -5,11 +5,10 @@ */ import { IndicesPutTemplateParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const setTemplate = async ( - callWithRequest: CallWithRequest, + callWithRequest: CallWithRequest, name: string, body: unknown ): Promise => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts index a93be40738e57..01819eb4703fb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts @@ -7,7 +7,7 @@ import { CallWithRequest } from '../types'; export const readPrivileges = async ( - callWithRequest: CallWithRequest, + callWithRequest: CallWithRequest<{}, unknown>, index: string ): Promise => { return callWithRequest('transport.request', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts deleted file mode 100644 index 5b85012fd9f08..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts +++ /dev/null @@ -1,113 +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 Hapi from 'hapi'; -import { KibanaConfig } from 'src/legacy/server/kbn_server'; -import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; -import { savedObjectsClientMock } from '../../../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../../alerting/server/alerts_client.mock'; -import { actionsClientMock } from '../../../../../../../../plugins/actions/server/mocks'; -import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../../common/constants'; -import { ServerFacade } from '../../../../types'; - -const defaultConfig = { - 'kibana.index': '.kibana', - [`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`]: '.siem-signals', -}; - -const isKibanaConfig = (config: unknown): config is KibanaConfig => - Object.getOwnPropertyDescriptor(config, 'get') != null && - Object.getOwnPropertyDescriptor(config, 'has') != null; - -const assertNever = (): never => { - throw new Error('Unexpected object'); -}; - -const createMockKibanaConfig = (config: Record): KibanaConfig => { - const returnConfig = { - get(key: string) { - return config[key]; - }, - has(key: string) { - return config[key] != null; - }, - }; - if (isKibanaConfig(returnConfig)) { - return returnConfig; - } else { - return assertNever(); - } -}; - -export const createMockServer = (config: Record = defaultConfig) => { - const server = new Hapi.Server({ - port: 0, - }); - - server.config = () => createMockKibanaConfig(config); - - const actionsClient = actionsClientMock.create(); - const alertsClient = alertsClientMock.create(); - const savedObjectsClient = savedObjectsClientMock.create(); - const elasticsearch = { - getCluster: jest.fn().mockImplementation(() => ({ - callWithRequest: jest.fn(), - })), - }; - server.decorate('request', 'getAlertsClient', () => alertsClient); - server.plugins.elasticsearch = (elasticsearch as unknown) as ElasticsearchPlugin; - server.plugins.spaces = { getSpaceId: () => 'default' }; - server.plugins.actions = { - getActionsClientWithRequest: () => actionsClient, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; // The types have really bad conflicts at the moment so I have to use any - server.decorate('request', 'getSavedObjectsClient', () => savedObjectsClient); - return { - server: server as ServerFacade & Hapi.Server, - alertsClient, - actionsClient, - elasticsearch, - savedObjectsClient, - }; -}; - -export const createMockServerWithoutAlertClientDecoration = ( - config: Record = defaultConfig -) => { - const serverWithoutAlertClient = new Hapi.Server({ - port: 0, - }); - - const savedObjectsClient = savedObjectsClientMock.create(); - serverWithoutAlertClient.config = () => createMockKibanaConfig(config); - serverWithoutAlertClient.decorate('request', 'getSavedObjectsClient', () => savedObjectsClient); - serverWithoutAlertClient.plugins.actions = { - getActionsClientWithRequest: () => actionsClient, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; // The types have really bad conflicts at the moment so I have to use any - - const actionsClient = actionsClientMock.create(); - - return { - serverWithoutAlertClient: serverWithoutAlertClient as ServerFacade & Hapi.Server, - actionsClient, - }; -}; - -export const getMockIndexName = () => - jest.fn().mockImplementation(() => ({ - callWithRequest: jest.fn().mockImplementationOnce(() => 'index-name'), - })); - -export const getMockEmptyIndex = () => - jest.fn().mockImplementation(() => ({ - callWithRequest: jest.fn().mockImplementation(() => ({ _shards: { total: 0 } })), - })); - -export const getMockNonEmptyIndex = () => - jest.fn().mockImplementation(() => ({ - callWithRequest: jest.fn().mockImplementation(() => ({ _shards: { total: 1 } })), - })); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/clients_service_mock.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/clients_service_mock.ts new file mode 100644 index 0000000000000..f89e938b8a636 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/clients_service_mock.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + elasticsearchServiceMock, + savedObjectsClientMock, +} from '../../../../../../../../../src/core/server/mocks'; +import { alertsClientMock } from '../../../../../../alerting/server/alerts_client.mock'; +import { ActionsClient } from '../../../../../../../../plugins/actions/server'; +import { actionsClientMock } from '../../../../../../../../plugins/actions/server/mocks'; +import { GetScopedClients } from '../../../../services'; + +const createClients = () => ({ + actionsClient: actionsClientMock.create() as jest.Mocked, + alertsClient: alertsClientMock.create(), + clusterClient: elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClient: savedObjectsClientMock.create(), + spacesClient: { getSpaceId: jest.fn() }, +}); + +const createGetScoped = () => + jest.fn(() => Promise.resolve(createClients()) as ReturnType); + +const createClientsServiceMock = () => { + return { + setup: jest.fn(), + start: jest.fn(), + createGetScoped, + }; +}; + +export const clientsServiceMock = { + create: createClientsServiceMock, + createGetScoped, + createClients, +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/index.ts new file mode 100644 index 0000000000000..250b006814294 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 Hapi from 'hapi'; + +export { clientsServiceMock } from './clients_service_mock'; + +export const createMockServer = () => { + const server = new Hapi.Server({ port: 0 }); + + return { + route: server.route.bind(server), + inject: server.inject.bind(server), + }; +}; + +export const createMockConfig = () => () => ({ + get: jest.fn(), + has: jest.fn(), +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index b008ead8df948..f380b82c1e05f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -17,6 +17,7 @@ import { INTERNAL_IMMUTABLE_KEY, DETECTION_ENGINE_PREPACKAGED_URL, } from '../../../../../common/constants'; +import { ShardsResponse } from '../../../types'; import { RuleAlertType, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { RuleAlertParamsRest, PrepackagedRules } from '../../types'; @@ -413,3 +414,11 @@ export const getFindResultStatus = (): SavedObjectsFindResponse 'index-name'; +export const getEmptyIndex = (): { _shards: Partial } => ({ + _shards: { total: 0 }, +}); +export const getNonEmptyIndex = (): { _shards: Partial } => ({ + _shards: { total: 1 }, +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts index e0d48836013ec..2502009a2e6a2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts @@ -7,9 +7,9 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; -import signalsPolicy from './signals_policy.json'; -import { ServerFacade, RequestFacade } from '../../../../types'; -import { transformError, getIndex, callWithRequestFactory } from '../utils'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; +import { transformError, getIndex } from '../utils'; import { getIndexExists } from '../../index/get_index_exists'; import { getPolicyExists } from '../../index/get_policy_exists'; import { setPolicy } from '../../index/set_policy'; @@ -17,8 +17,12 @@ import { setTemplate } from '../../index/set_template'; import { getSignalsTemplate } from './get_signals_template'; import { getTemplateExists } from '../../index/get_template_exists'; import { createBootstrapIndex } from '../../index/create_bootstrap_index'; +import signalsPolicy from './signals_policy.json'; -export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createCreateIndexRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: DETECTION_ENGINE_INDEX_URL, @@ -30,11 +34,13 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }, }, }, - async handler(request: RequestFacade, headers) { + async handler(request: LegacyRequest, headers) { try { - const index = getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, index); + const { clusterClient, spacesClient } = await getClients(request); + const callCluster = clusterClient.callAsCurrentUser; + + const index = getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists(callCluster, index); if (indexExists) { return headers .response({ @@ -43,16 +49,16 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }) .code(409); } else { - const policyExists = await getPolicyExists(callWithRequest, index); + const policyExists = await getPolicyExists(callCluster, index); if (!policyExists) { - await setPolicy(callWithRequest, index, signalsPolicy); + await setPolicy(callCluster, index, signalsPolicy); } - const templateExists = await getTemplateExists(callWithRequest, index); + const templateExists = await getTemplateExists(callCluster, index); if (!templateExists) { const template = getSignalsTemplate(index); - await setTemplate(callWithRequest, index, template); + await setTemplate(callCluster, index, template); } - await createBootstrapIndex(callWithRequest, index); + await createBootstrapIndex(callCluster, index); return { acknowledged: true }; } } catch (err) { @@ -68,6 +74,10 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const createIndexRoute = (server: ServerFacade) => { - server.route(createCreateIndexRoute(server)); +export const createIndexRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(createCreateIndexRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts index c1edc824b81eb..ae61afb6f8d06 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts @@ -7,8 +7,9 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../../types'; -import { transformError, getIndex, callWithRequestFactory } from '../utils'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; +import { transformError, getIndex } from '../utils'; import { getIndexExists } from '../../index/get_index_exists'; import { getPolicyExists } from '../../index/get_policy_exists'; import { deletePolicy } from '../../index/delete_policy'; @@ -26,7 +27,10 @@ import { deleteTemplate } from '../../index/delete_template'; * * And ensuring they're all gone */ -export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createDeleteIndexRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'DELETE', path: DETECTION_ENGINE_INDEX_URL, @@ -38,11 +42,13 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }, }, }, - async handler(request: RequestFacade, headers) { + async handler(request: LegacyRequest, headers) { try { - const index = getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, index); + const { clusterClient, spacesClient } = await getClients(request); + const callCluster = clusterClient.callAsCurrentUser; + + const index = getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists(callCluster, index); if (!indexExists) { return headers .response({ @@ -51,14 +57,14 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }) .code(404); } else { - await deleteAllIndex(callWithRequest, `${index}-*`); - const policyExists = await getPolicyExists(callWithRequest, index); + await deleteAllIndex(callCluster, `${index}-*`); + const policyExists = await getPolicyExists(callCluster, index); if (policyExists) { - await deletePolicy(callWithRequest, index); + await deletePolicy(callCluster, index); } - const templateExists = await getTemplateExists(callWithRequest, index); + const templateExists = await getTemplateExists(callCluster, index); if (templateExists) { - await deleteTemplate(callWithRequest, index); + await deleteTemplate(callCluster, index); } return { acknowledged: true }; } @@ -75,6 +81,10 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const deleteIndexRoute = (server: ServerFacade) => { - server.route(createDeleteIndexRoute(server)); +export const deleteIndexRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(createDeleteIndexRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts index 1a5018d446747..41be42f7c0fe1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts @@ -7,11 +7,15 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../../types'; -import { transformError, getIndex, callWithRequestFactory } from '../utils'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; +import { transformError, getIndex } from '../utils'; import { getIndexExists } from '../../index/get_index_exists'; -export const createReadIndexRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createReadIndexRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'GET', path: DETECTION_ENGINE_INDEX_URL, @@ -23,11 +27,14 @@ export const createReadIndexRoute = (server: ServerFacade): Hapi.ServerRoute => }, }, }, - async handler(request: RequestFacade, headers) { + async handler(request: LegacyRequest, headers) { try { - const index = getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, index); + const { clusterClient, spacesClient } = await getClients(request); + const callCluster = clusterClient.callAsCurrentUser; + + const index = getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists(callCluster, index); + if (indexExists) { // head request is used for if you want to get if the index exists // or not and it will return a content-length: 0 along with either a 200 or 404 @@ -62,6 +69,10 @@ export const createReadIndexRoute = (server: ServerFacade): Hapi.ServerRoute => }; }; -export const readIndexRoute = (server: ServerFacade) => { - server.route(createReadIndexRoute(server)); +export const readIndexRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(createReadIndexRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts index 1ea681afb7949..308ee95a77e20 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts @@ -4,35 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMockServer } from '../__mocks__/_mock_server'; -import { getPrivilegeRequest, getMockPrivileges } from '../__mocks__/request_responses'; import { readPrivilegesRoute } from './read_privileges_route'; -import * as myUtils from '../utils'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; +import { getPrivilegeRequest, getMockPrivileges } from '../__mocks__/request_responses'; describe('read_privileges', () => { - let { server, elasticsearch } = createMockServer(); + let { route, inject } = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { - jest.spyOn(myUtils, 'getIndex').mockReturnValue('fakeindex'); - ({ server, elasticsearch } = createMockServer()); - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn(() => getMockPrivileges()), - })); - readPrivilegesRoute(server); - }); - - afterEach(() => { jest.resetAllMocks(); + ({ route, inject } = createMockServer()); + + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getMockPrivileges()); + + readPrivilegesRoute(route, config, false, getClients); }); describe('normal status codes', () => { test('returns 200 when doing a normal request', async () => { - const { statusCode } = await server.inject(getPrivilegeRequest()); + const { statusCode } = await inject(getPrivilegeRequest()); expect(statusCode).toBe(200); }); test('returns the payload when doing a normal request', async () => { - const { payload } = await server.inject(getPrivilegeRequest()); + const { payload } = await inject(getPrivilegeRequest()); expect(JSON.parse(payload)).toEqual(getMockPrivileges()); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 45ecb7dc97288..e9b9bffbaf054 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -6,13 +6,19 @@ import Hapi from 'hapi'; import { merge } from 'lodash/fp'; + import { DETECTION_ENGINE_PRIVILEGES_URL } from '../../../../../common/constants'; +import { LegacyServices } from '../../../../types'; import { RulesRequest } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; -import { callWithRequestFactory, transformError, getIndex } from '../utils'; +import { GetScopedClients } from '../../../../services'; +import { transformError, getIndex } from '../utils'; import { readPrivileges } from '../../privileges/read_privileges'; -export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createReadPrivilegesRulesRoute = ( + config: LegacyServices['config'], + usingEphemeralEncryptionKey: boolean, + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'GET', path: DETECTION_ENGINE_PRIVILEGES_URL, @@ -26,10 +32,10 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve }, async handler(request: RulesRequest, headers) { try { - const callWithRequest = callWithRequestFactory(request, server); - const index = getIndex(request, server); - const permissions = await readPrivileges(callWithRequest, index); - const usingEphemeralEncryptionKey = server.usingEphemeralEncryptionKey; + const { clusterClient, spacesClient } = await getClients(request); + + const index = getIndex(spacesClient.getSpaceId, config); + const permissions = await readPrivileges(clusterClient.callAsCurrentUser, index); return merge(permissions, { is_authenticated: request?.auth?.isAuthenticated ?? false, has_encryption_key: !usingEphemeralEncryptionKey, @@ -47,6 +53,11 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve }; }; -export const readPrivilegesRoute = (server: ServerFacade): void => { - server.route(createReadPrivilegesRulesRoute(server)); +export const readPrivilegesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + usingEphemeralEncryptionKey: boolean, + getClients: GetScopedClients +) => { + route(createReadPrivilegesRulesRoute(config, usingEphemeralEncryptionKey, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index ec86de84ff3c7..e018ed4cc22ff 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -4,12 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, - getMockEmptyIndex, - getMockNonEmptyIndex, -} from '../__mocks__/_mock_server'; +import { omit } from 'lodash/fp'; + import { createRulesRoute } from './create_rules_route'; import { getFindResult, @@ -17,7 +13,10 @@ import { createActionResult, addPrepackagedRulesRequest, getFindResultWithSingleHit, + getEmptyIndex, + getNonEmptyIndex, } from '../__mocks__/request_responses'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -48,45 +47,56 @@ import { addPrepackedRulesRoute } from './add_prepackaged_rules_route'; import { PrepackagedRules } from '../../types'; describe('add_prepackaged_rules_route', () => { - let { server, alertsClient, actionsClient, elasticsearch } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); - elasticsearch.getCluster = getMockNonEmptyIndex(); - addPrepackedRulesRoute(server); + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); + + addPrepackedRulesRoute(server.route, config, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when creating a with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { statusCode } = await server.inject(addPrepackagedRulesRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(addPrepackagedRulesRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { inject, route } = createMockServer(); + createRulesRoute(route, config, getClients); + const { statusCode } = await inject(addPrepackagedRulesRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('it returns a 400 if the index does not exist', async () => { - elasticsearch.getCluster = getMockEmptyIndex(); - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(addPrepackagedRulesRequest()); expect(JSON.parse(payload)).toEqual({ - message: - 'Pre-packaged rules cannot be installed until the space index is created: .siem-signals-default', + message: expect.stringContaining( + 'Pre-packaged rules cannot be installed until the space index is created' + ), status_code: 400, }); }); @@ -94,10 +104,10 @@ describe('add_prepackaged_rules_route', () => { describe('payload', () => { test('1 rule is installed and 0 are updated when find results are empty', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(addPrepackagedRulesRequest()); expect(JSON.parse(payload)).toEqual({ rules_installed: 1, @@ -106,10 +116,10 @@ describe('add_prepackaged_rules_route', () => { }); test('1 rule is updated and 0 are installed when we return a single find and the versions are different', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(addPrepackagedRulesRequest()); expect(JSON.parse(payload)).toEqual({ rules_installed: 0, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index e796f21d9c03a..c4d0489486ef8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -5,21 +5,23 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { getIndexExists } from '../../index/get_index_exists'; -import { callWithRequestFactory, getIndex, transformError } from '../utils'; +import { getIndex, transformError } from '../utils'; import { getPrepackagedRules } from '../../rules/get_prepackaged_rules'; import { installPrepackagedRules } from '../../rules/install_prepacked_rules'; import { updatePrepackagedRules } from '../../rules/update_prepacked_rules'; import { getRulesToInstall } from '../../rules/get_rules_to_install'; import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createAddPrepackedRulesRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'PUT', path: DETECTION_ENGINE_PREPACKAGED_URL, @@ -31,29 +33,32 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR }, }, }, - async handler(request: RequestFacade, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } - + async handler(request: LegacyRequest, headers) { try { - const callWithRequest = callWithRequestFactory(request, server); + const { + actionsClient, + alertsClient, + clusterClient, + savedObjectsClient, + spacesClient, + } = await getClients(request); + + if (!actionsClient || !alertsClient) { + return headers.response().code(404); + } + const rulesFromFileSystem = getPrepackagedRules(); const prepackagedRules = await getExistingPrepackagedRules({ alertsClient }); const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules); const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules); - const spaceIndex = getIndex(request, server); + const spaceIndex = getIndex(spacesClient.getSpaceId, config); if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) { - const spaceIndexExists = await getIndexExists(callWithRequest, spaceIndex); + const spaceIndexExists = await getIndexExists( + clusterClient.callAsCurrentUser, + spaceIndex + ); if (!spaceIndexExists) { return headers .response({ @@ -90,6 +95,10 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR }; }; -export const addPrepackedRulesRoute = (server: ServerFacade): void => { - server.route(createAddPrepackedRulesRoute(server)); +export const addPrepackedRulesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createAddPrepackedRulesRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index f1169442484c6..664d27a7572ad 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -4,59 +4,66 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, - getMockEmptyIndex, -} from '../__mocks__/_mock_server'; -import { createRulesRoute } from './create_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; + import { getFindResult, getResult, createActionResult, typicalPayload, getReadBulkRequest, + getEmptyIndex, } from '../__mocks__/request_responses'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRulesBulkRoute } from './create_rules_bulk_route'; import { BulkError } from '../utils'; import { OutputRuleAlertRest } from '../../types'; describe('create_rules_bulk', () => { - let { server, alertsClient, actionsClient, elasticsearch } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); - createRulesBulkRoute(server); + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + getClients.mockResolvedValue(clients); + + createRulesBulkRoute(server.route, config, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getReadBulkRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getReadBulkRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { inject, route } = createMockServer(); + createRulesBulkRoute(route, config, getClients); + const { statusCode } = await inject(getReadBulkRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('it gets a 409 if the index does not exist', async () => { - elasticsearch.getCluster = getMockEmptyIndex(); - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(getReadBulkRequest()); expect(JSON.parse(payload)).toEqual([ { @@ -71,10 +78,10 @@ describe('create_rules_bulk', () => { }); test('returns 200 if rule_id is not given as the id is auto generated from the alert framework', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); // missing rule_id should return 200 as it will be auto generated if not given const { rule_id, ...noRuleId } = typicalPayload(); const request: ServerInjectOptions = { @@ -87,10 +94,10 @@ describe('create_rules_bulk', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', @@ -107,10 +114,10 @@ describe('create_rules_bulk', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', @@ -128,10 +135,10 @@ describe('create_rules_bulk', () => { }); test('returns 409 if duplicate rule_ids found in request payload', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'POST', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, @@ -143,10 +150,10 @@ describe('create_rules_bulk', () => { }); test('returns one error object in response when duplicate rule_ids found in request payload', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'POST', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index e7145d2a6f055..51b7b132fc794 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -5,25 +5,24 @@ */ import Hapi from 'hapi'; -import { isFunction, countBy } from 'lodash/fp'; +import { countBy } from 'lodash/fp'; import uuid from 'uuid'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { GetScopedClients } from '../../../../services'; +import { LegacyServices } from '../../../../types'; import { createRules } from '../../rules/create_rules'; import { BulkRulesRequest } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; import { readRules } from '../../rules/read_rules'; import { transformOrBulkError, getDuplicates } from './utils'; import { getIndexExists } from '../../index/get_index_exists'; -import { - callWithRequestFactory, - getIndex, - transformBulkError, - createBulkErrorObject, -} from '../utils'; +import { getIndex, transformBulkError, createBulkErrorObject } from '../utils'; import { createRulesBulkSchema } from '../schemas/create_rules_bulk_schema'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createCreateRulesBulkRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, @@ -37,14 +36,11 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }, }, async handler(request: BulkRulesRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) + const { actionsClient, alertsClient, clusterClient, spacesClient } = await getClients( + request ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + + if (!actionsClient || !alertsClient) { return headers.response().code(404); } @@ -85,9 +81,8 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou } = payloadRule; const ruleIdOrUuid = ruleId ?? uuid.v4(); try { - const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, finalIndex); + const finalIndex = outputIndex ?? getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); if (!indexExists) { return createBulkErrorObject({ ruleId: ruleIdOrUuid, @@ -155,6 +150,10 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }; }; -export const createRulesBulkRoute = (server: ServerFacade): void => { - server.route(createCreateRulesBulkRoute(server)); +export const createRulesBulkRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createCreateRulesBulkRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index e51634c0d2c07..4f28771db8ed7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, - getMockNonEmptyIndex, - getMockEmptyIndex, -} from '../__mocks__/_mock_server'; -import { createRulesRoute } from './create_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { createRulesRoute } from './create_rules_route'; import { getFindResult, @@ -20,57 +17,58 @@ import { getCreateRequest, typicalPayload, getFindResultStatus, + getNonEmptyIndex, + getEmptyIndex, } from '../__mocks__/request_responses'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; describe('create_rules', () => { - let { - server, - alertsClient, - actionsClient, - elasticsearch, - savedObjectsClient, - } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ - server, - alertsClient, - actionsClient, - elasticsearch, - savedObjectsClient, - } = createMockServer()); - elasticsearch.getCluster = getMockNonEmptyIndex(); - createRulesRoute(server); + + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); + + createRulesRoute(server.route, config, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getCreateRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getCreateRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + createRulesRoute(route, config, getClients); + const { statusCode } = await inject(getCreateRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('it returns a 400 if the index does not exist', async () => { - elasticsearch.getCluster = getMockEmptyIndex(); - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(getCreateRequest()); expect(JSON.parse(payload)).toEqual({ message: 'To create a rule, the index must exist first. Index .siem-signals does not exist', @@ -79,11 +77,11 @@ describe('create_rules', () => { }); test('returns 200 if rule_id is not given as the id is auto generated from the alert framework', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); // missing rule_id should return 200 as it will be auto generated if not given const { rule_id, ...noRuleId } = typicalPayload(); const request: ServerInjectOptions = { @@ -96,11 +94,11 @@ describe('create_rules', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', @@ -115,11 +113,11 @@ describe('create_rules', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index de874f66d0444..19e772165628d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -5,21 +5,24 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import uuid from 'uuid'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { GetScopedClients } from '../../../../services'; +import { LegacyServices } from '../../../../types'; import { createRules } from '../../rules/create_rules'; import { RulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { createRulesSchema } from '../schemas/create_rules_schema'; -import { ServerFacade } from '../../../../types'; import { readRules } from '../../rules/read_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { transform } from './utils'; import { getIndexExists } from '../../index/get_index_exists'; -import { callWithRequestFactory, getIndex, transformError } from '../utils'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; +import { getIndex, transformError } from '../utils'; -export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createCreateRulesRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: DETECTION_ENGINE_RULES_URL, @@ -59,21 +62,21 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = type, references, } = request.payload; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } - try { - const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, finalIndex); + const { + alertsClient, + actionsClient, + clusterClient, + savedObjectsClient, + spacesClient, + } = await getClients(request); + + if (!actionsClient || !alertsClient) { + return headers.response().code(404); + } + + const finalIndex = outputIndex ?? getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); if (!indexExists) { return headers .response({ @@ -157,6 +160,10 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const createRulesRoute = (server: ServerFacade): void => { - server.route(createCreateRulesRoute(server)); +export const createRulesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createCreateRulesRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts index e66fc765c08bf..855bf7f634c26 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts @@ -4,10 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; +import { omit } from 'lodash/fp'; import { ServerInjectOptions } from 'hapi'; import { @@ -20,70 +17,75 @@ import { getDeleteAsPostBulkRequestById, getFindResultStatus, } from '../__mocks__/request_responses'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { deleteRulesBulkRoute } from './delete_rules_bulk_route'; import { BulkError } from '../utils'; describe('delete_rules', () => { - let { server, alertsClient, savedObjectsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { - ({ server, alertsClient, savedObjectsClient } = createMockServer()); - deleteRulesBulkRoute(server); - }); - - afterEach(() => { jest.resetAllMocks(); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + deleteRulesBulkRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteBulkRequest()); expect(statusCode).toBe(200); }); test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId using POST', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteAsPostBulkRequest()); expect(statusCode).toBe(200); }); test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteBulkRequestById()); expect(statusCode).toBe(200); }); test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id using POST', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteAsPostBulkRequestById()); expect(statusCode).toBe(200); }); test('returns 200 because the error is in the payload when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteBulkRequest()); expect(statusCode).toBe(200); }); test('returns 404 in the payload when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); - savedObjectsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue({}); const { payload } = await server.inject(getDeleteBulkRequest()); const parsed: BulkError[] = JSON.parse(payload); const expected: BulkError[] = [ @@ -96,18 +98,19 @@ describe('delete_rules', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - deleteRulesBulkRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getDeleteBulkRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + deleteRulesBulkRoute(route, getClients); + const { statusCode } = await inject(getDeleteBulkRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if given a non-existent id in the payload', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const request: ServerInjectOptions = { method: 'DELETE', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index b3f8eafa24115..6438318cb43db 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -5,19 +5,18 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { deleteRules } from '../../rules/delete_rules'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { queryRulesBulkSchema } from '../schemas/query_rules_bulk_schema'; import { transformOrBulkError, getIdBulkError } from './utils'; import { transformBulkError } from '../utils'; import { QueryBulkRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; +import { deleteRules } from '../../rules/delete_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createDeleteRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createDeleteRulesBulkRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { method: ['POST', 'DELETE'], // allow both POST and DELETE in case their client does not support bodies in DELETE path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, @@ -31,14 +30,9 @@ export const createDeleteRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }, }, async handler(request: QueryBulkRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + const { actionsClient, alertsClient, savedObjectsClient } = await getClients(request); + + if (!actionsClient || !alertsClient) { return headers.response().code(404); } const rules = await Promise.all( @@ -78,6 +72,9 @@ export const createDeleteRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }; }; -export const deleteRulesBulkRoute = (server: ServerFacade): void => { - server.route(createDeleteRulesBulkRoute(server)); +export const deleteRulesBulkRoute = ( + route: LegacyServices['route'], + getClients: GetScopedClients +): void => { + route(createDeleteRulesBulkRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index 0aa60d3bbd922..a0a6f61223279 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { deleteRulesRoute } from './delete_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; +import { deleteRulesRoute } from './delete_rules_route'; import { getFindResult, @@ -20,64 +16,70 @@ import { getDeleteRequestById, getFindResultStatus, } from '../__mocks__/request_responses'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('delete_rules', () => { - let { server, alertsClient, savedObjectsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { - ({ server, alertsClient, savedObjectsClient } = createMockServer()); - deleteRulesRoute(server); - }); - - afterEach(() => { jest.resetAllMocks(); + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + + deleteRulesRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); - savedObjectsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteRequest()); expect(statusCode).toBe(200); }); test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); - savedObjectsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteRequestById()); expect(statusCode).toBe(200); }); test('returns 404 when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); - savedObjectsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - deleteRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getDeleteRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + deleteRulesRoute(route, getClients); + const { statusCode } = await inject(getDeleteRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if given a non-existent id', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const request: ServerInjectOptions = { method: 'DELETE', url: DETECTION_ENGINE_RULES_URL, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts index e4d3787c90072..340782523b724 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -5,19 +5,18 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { deleteRules } from '../../rules/delete_rules'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { queryRulesSchema } from '../schemas/query_rules_schema'; import { getIdError, transform } from './utils'; import { transformError } from '../utils'; import { QueryRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createDeleteRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createDeleteRulesRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { method: 'DELETE', path: DETECTION_ENGINE_RULES_URL, @@ -30,20 +29,16 @@ export const createDeleteRulesRoute = (server: ServerFacade): Hapi.ServerRoute = query: queryRulesSchema, }, }, - async handler(request: QueryRequest, headers) { + async handler(request: QueryRequest & LegacyRequest, headers) { const { id, rule_id: ruleId } = request.query; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } try { + const { actionsClient, alertsClient, savedObjectsClient } = await getClients(request); + + if (!actionsClient || !alertsClient) { + return headers.response().code(404); + } + const rule = await deleteRules({ actionsClient, alertsClient, @@ -95,6 +90,9 @@ export const createDeleteRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const deleteRulesRoute = (server: ServerFacade): void => { - server.route(createDeleteRulesRoute(server)); +export const deleteRulesRoute = ( + route: LegacyServices['route'], + getClients: GetScopedClients +): void => { + route(createDeleteRulesRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts index 5da5ffcd58bf1..1966b06701803 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -5,17 +5,21 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { ExportRulesRequest } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; import { getNonPackagedRulesCount } from '../../rules/get_existing_prepackaged_rules'; import { exportRulesSchema, exportRulesQuerySchema } from '../schemas/export_rules_schema'; import { getExportByObjectIds } from '../../rules/get_export_by_object_ids'; import { getExportAll } from '../../rules/get_export_all'; import { transformError } from '../utils'; -export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createExportRulesRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: `${DETECTION_ENGINE_RULES_URL}/_export`, @@ -29,15 +33,15 @@ export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = query: exportRulesQuerySchema, }, }, - async handler(request: ExportRulesRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + async handler(request: ExportRulesRequest & LegacyRequest, headers) { + const { alertsClient } = await getClients(request); if (!alertsClient) { return headers.response().code(404); } try { - const exportSizeLimit = server.config().get('savedObjects.maxImportExportSize'); + const exportSizeLimit = config().get('savedObjects.maxImportExportSize'); if (request.payload?.objects != null && request.payload.objects.length > exportSizeLimit) { return headers .response({ @@ -82,6 +86,10 @@ export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const exportRulesRoute = (server: ServerFacade): void => { - server.route(createExportRulesRoute(server)); +export const exportRulesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createExportRulesRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts index 62c9f44da1e33..5b75f17164acf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; +import { omit } from 'lodash/fp'; + +import { createMockServer } from '../__mocks__'; +import { clientsServiceMock } from '../__mocks__/clients_service_mock'; import { findRulesRoute } from './find_rules_route'; import { ServerInjectOptions } from 'hapi'; @@ -16,43 +16,49 @@ import { getFindResult, getResult, getFindRequest } from '../__mocks__/request_r import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('find_rules', () => { - let { server, alertsClient, actionsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { - ({ server, alertsClient, actionsClient } = createMockServer()); - findRulesRoute(server); - }); - - afterEach(() => { jest.resetAllMocks(); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + + findRulesRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when finding a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.find.mockResolvedValue({ + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.find.mockResolvedValue({ page: 1, perPage: 1, total: 0, data: [], }); - alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getFindRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - findRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getFindRequest()); + const { route, inject } = createMockServer(); + getClients.mockResolvedValue(omit('alertsClient', clients)); + findRulesRoute(route, getClients); + const { statusCode } = await inject(getFindRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if a bad query parameter is given', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'GET', url: `${DETECTION_ENGINE_RULES_URL}/_find?invalid_value=500`, @@ -62,8 +68,8 @@ describe('find_rules', () => { }); test('returns 200 if the set of optional query parameters are given', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'GET', url: `${DETECTION_ENGINE_RULES_URL}/_find?page=2&per_page=20&sort_field=timestamp&fields=["field-1","field-2","field-3]`, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts index b15c1db7222cf..4297e4aebfd58 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -5,17 +5,17 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { findRules } from '../../rules/find_rules'; import { FindRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { findRulesSchema } from '../schemas/find_rules_schema'; -import { ServerFacade } from '../../../../types'; import { transformFindAlerts } from './utils'; import { transformError } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -export const createFindRulesRoute = (): Hapi.ServerRoute => { +export const createFindRulesRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { method: 'GET', path: `${DETECTION_ENGINE_RULES_URL}/_find`, @@ -28,17 +28,14 @@ export const createFindRulesRoute = (): Hapi.ServerRoute => { query: findRulesSchema, }, }, - async handler(request: FindRulesRequest, headers) { + async handler(request: FindRulesRequest & LegacyRequest, headers) { const { query } = request; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } - try { + const { alertsClient, savedObjectsClient } = await getClients(request); + if (!alertsClient) { + return headers.response().code(404); + } + const rules = await findRules({ alertsClient, perPage: query.per_page, @@ -86,6 +83,6 @@ export const createFindRulesRoute = (): Hapi.ServerRoute => { }; }; -export const findRulesRoute = (server: ServerFacade) => { - server.route(createFindRulesRoute()); +export const findRulesRoute = (route: LegacyServices['route'], getClients: GetScopedClients) => { + route(createFindRulesRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index 8b3113a044b5a..fe8742ff0b60c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -5,10 +5,11 @@ */ import Hapi from 'hapi'; -import { isFunction, snakeCase } from 'lodash/fp'; +import { snakeCase } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { findRulesStatusesSchema } from '../schemas/find_rules_statuses_schema'; import { FindRulesStatusesRequest, @@ -29,7 +30,7 @@ const convertToSnakeCase = >(obj: T): Partial | }, {}); }; -export const createFindRulesStatusRoute: Hapi.ServerRoute = { +export const createFindRulesStatusRoute = (getClients: GetScopedClients): Hapi.ServerRoute => ({ method: 'GET', path: `${DETECTION_ENGINE_RULES_URL}/_find_statuses`, options: { @@ -41,19 +42,17 @@ export const createFindRulesStatusRoute: Hapi.ServerRoute = { query: findRulesStatusesSchema, }, }, - async handler(request: FindRulesStatusesRequest, headers) { + async handler(request: FindRulesStatusesRequest & LegacyRequest, headers) { const { query } = request; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + const { alertsClient, savedObjectsClient } = await getClients(request); + + if (!alertsClient) { return headers.response().code(404); } // build return object with ids as keys and errors as values. /* looks like this - { + { "someAlertId": [{"myerrorobject": "some error value"}, etc..], "anotherAlertId": ... } @@ -86,8 +85,11 @@ export const createFindRulesStatusRoute: Hapi.ServerRoute = { }, Promise.resolve({})); return statuses; }, -}; +}); -export const findRulesStatusesRoute = (server: ServerFacade): void => { - server.route(createFindRulesStatusRoute); +export const findRulesStatusesRoute = ( + route: LegacyServices['route'], + getClients: GetScopedClients +): void => { + route(createFindRulesStatusRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts index de7f0fe26cc74..8f27910a7e5e2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts @@ -4,19 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, - getMockNonEmptyIndex, -} from '../__mocks__/_mock_server'; -import { createRulesRoute } from './create_rules_route'; +import { omit } from 'lodash/fp'; + +import { getPrepackagedRulesStatusRoute } from './get_prepackaged_rules_status_route'; + import { getFindResult, getResult, createActionResult, getFindResultWithSingleHit, getPrepackagedRulesStatusRequest, + getNonEmptyIndex, } from '../__mocks__/request_responses'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -41,44 +41,49 @@ jest.mock('../../rules/get_prepackaged_rules', () => { }; }); -import { getPrepackagedRulesStatusRoute } from './get_prepackaged_rules_status_route'; - describe('get_prepackaged_rule_status_route', () => { - let { server, alertsClient, actionsClient, elasticsearch } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); - elasticsearch.getCluster = getMockNonEmptyIndex(); - getPrepackagedRulesStatusRoute(server); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); + + getPrepackagedRulesStatusRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when creating a with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getPrepackagedRulesStatusRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject( - getPrepackagedRulesStatusRequest() - ); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + getPrepackagedRulesStatusRoute(route, getClients); + const { statusCode } = await inject(getPrepackagedRulesStatusRequest()); expect(statusCode).toBe(404); }); }); describe('payload', () => { test('0 rules installed, 0 custom rules, 1 rules not installed, and 1 rule not updated', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(getPrepackagedRulesStatusRequest()); expect(JSON.parse(payload)).toEqual({ rules_custom_installed: 0, @@ -89,10 +94,10 @@ describe('get_prepackaged_rule_status_route', () => { }); test('1 rule installed, 1 custom rules, 0 rules not installed, and 1 rule to not updated', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(getPrepackagedRulesStatusRequest()); expect(JSON.parse(payload)).toEqual({ rules_custom_installed: 1, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index c999292ba7674..bee57d6b38127 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -5,10 +5,10 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { transformError } from '../utils'; import { getPrepackagedRules } from '../../rules/get_prepackaged_rules'; import { getRulesToInstall } from '../../rules/get_rules_to_install'; @@ -16,7 +16,9 @@ import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { findRules } from '../../rules/find_rules'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; -export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => { +export const createGetPrepackagedRulesStatusRoute = ( + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'GET', path: `${DETECTION_ENGINE_PREPACKAGED_URL}/_status`, @@ -28,8 +30,8 @@ export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => { }, }, }, - async handler(request: RequestFacade, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + async handler(request: LegacyRequest, headers) { + const { alertsClient } = await getClients(request); if (!alertsClient) { return headers.response().code(404); @@ -67,6 +69,9 @@ export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => { }; }; -export const getPrepackagedRulesStatusRoute = (server: ServerFacade): void => { - server.route(createGetPrepackagedRulesStatusRoute()); +export const getPrepackagedRulesStatusRoute = ( + route: LegacyServices['route'], + getClients: GetScopedClients +): void => { + route(createGetPrepackagedRulesStatusRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 5e87c99d815ef..a9188cc2adc12 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -5,40 +5,39 @@ */ import Hapi from 'hapi'; -import { chunk, isEmpty, isFunction } from 'lodash/fp'; +import { chunk, isEmpty } from 'lodash/fp'; import { extname } from 'path'; import uuid from 'uuid'; + import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { LegacyServices, LegacyRequest } from '../../../../types'; import { createRules } from '../../rules/create_rules'; import { ImportRulesRequest } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; import { readRules } from '../../rules/read_rules'; import { getIndexExists } from '../../index/get_index_exists'; -import { - callWithRequestFactory, - getIndex, - createBulkErrorObject, - ImportRuleResponse, -} from '../utils'; +import { getIndex, createBulkErrorObject, ImportRuleResponse } from '../utils'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { ImportRuleAlertRest } from '../../types'; import { patchRules } from '../../rules/patch_rules'; import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; +import { GetScopedClients } from '../../../../services'; type PromiseFromStreams = ImportRuleAlertRest | Error; const CHUNK_PARSED_OBJECT_SIZE = 10; -export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createImportRulesRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: `${DETECTION_ENGINE_RULES_URL}/_import`, options: { tags: ['access:siem'], payload: { - maxBytes: server.config().get('savedObjects.maxImportPayloadBytes'), + maxBytes: config().get('savedObjects.maxImportPayloadBytes'), output: 'stream', allow: 'multipart/form-data', }, @@ -50,17 +49,19 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = payload: importRulesPayloadSchema, }, }, - async handler(request: ImportRulesRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + async handler(request: ImportRulesRequest & LegacyRequest, headers) { + const { + actionsClient, + alertsClient, + clusterClient, + spacesClient, + savedObjectsClient, + } = await getClients(request); + + if (!actionsClient || !alertsClient) { return headers.response().code(404); } + const { filename } = request.payload.file.hapi; const fileExtension = extname(filename).toLowerCase(); if (fileExtension !== '.ndjson') { @@ -72,7 +73,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = .code(400); } - const objectLimit = server.config().get('savedObjects.maxImportExportSize'); + const objectLimit = config().get('savedObjects.maxImportExportSize'); const readStream = createRulesStreamFromNdJson(request.payload.file, objectLimit); const parsedObjects = await createPromiseFromStreams([readStream]); const uniqueParsedObjects = Array.from( @@ -146,9 +147,11 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = version, } = parsedRule; try { - const finalIndex = getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, finalIndex); + const finalIndex = getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists( + clusterClient.callAsCurrentUser, + finalIndex + ); if (!indexExists) { resolve( createBulkErrorObject({ @@ -261,6 +264,10 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const importRulesRoute = (server: ServerFacade): void => { - server.route(createImportRulesRoute(server)); +export const importRulesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createImportRulesRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts index aa0dd04786a2e..02af4135b534f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { patchRulesRoute } from './patch_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { patchRulesRoute } from './patch_rules_route'; +import { omit } from 'lodash/fp'; import { getFindResult, @@ -20,43 +16,51 @@ import { getFindResultWithSingleHit, getPatchBulkRequest, } from '../__mocks__/request_responses'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { patchRulesBulkRoute } from './patch_rules_bulk_route'; import { BulkError } from '../utils'; describe('patch_rules_bulk', () => { - let { server, alertsClient, actionsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient } = createMockServer()); - patchRulesBulkRoute(server); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + patchRulesBulkRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getPatchBulkRequest()); expect(statusCode).toBe(200); }); test('returns 200 as a response when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getPatchBulkRequest()); expect(statusCode).toBe(200); }); test('returns 404 within the payload when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { payload } = await server.inject(getPatchBulkRequest()); const parsed: BulkError[] = JSON.parse(payload); const expected: BulkError[] = [ @@ -69,17 +73,18 @@ describe('patch_rules_bulk', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - patchRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getPatchBulkRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + patchRulesRoute(route, getClients); + const { statusCode } = await inject(getPatchBulkRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if id is not given in either the body or the url', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); const { rule_id, ...noId } = typicalPayload(); const request: ServerInjectOptions = { method: 'PATCH', @@ -91,9 +96,9 @@ describe('patch_rules_bulk', () => { }); test('returns errors as 200 to just indicate ok something happened', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PATCH', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -104,9 +109,9 @@ describe('patch_rules_bulk', () => { }); test('returns 404 in the payload if the record does not exist yet', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PATCH', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -124,10 +129,10 @@ describe('patch_rules_bulk', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PATCH', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -138,10 +143,10 @@ describe('patch_rules_bulk', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'PATCH', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 00184b6c16b7e..d3f92e9e05bcc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -5,21 +5,21 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { BulkPatchRulesRequest, IRuleSavedAttributesSavedObjectAttributes, } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { transformOrBulkError, getIdBulkError } from './utils'; import { transformBulkError } from '../utils'; import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema'; import { patchRules } from '../../rules/patch_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createPatchRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createPatchRulesBulkRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { method: 'PATCH', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -33,14 +33,9 @@ export const createPatchRulesBulkRoute = (server: ServerFacade): Hapi.ServerRout }, }, async handler(request: BulkPatchRulesRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + const { actionsClient, alertsClient, savedObjectsClient } = await getClients(request); + + if (!actionsClient || !alertsClient) { return headers.response().code(404); } @@ -132,6 +127,9 @@ export const createPatchRulesBulkRoute = (server: ServerFacade): Hapi.ServerRout }; }; -export const patchRulesBulkRoute = (server: ServerFacade): void => { - server.route(createPatchRulesBulkRoute(server)); +export const patchRulesBulkRoute = ( + route: LegacyServices['route'], + getClients: GetScopedClients +): void => { + route(createPatchRulesBulkRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index d315d45046e2d..cc84b08fdef11 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { patchRulesRoute } from './patch_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; +import { patchRulesRoute } from './patch_rules_route'; import { getFindResult, @@ -21,51 +17,59 @@ import { typicalPayload, getFindResultWithSingleHit, } from '../__mocks__/request_responses'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('patch_rules', () => { - let { server, alertsClient, actionsClient, savedObjectsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient, savedObjectsClient } = createMockServer()); - patchRulesRoute(server); + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + patchRulesRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getPatchRequest()); expect(statusCode).toBe(200); }); test('returns 404 when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getPatchRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - patchRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getPatchRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + patchRulesRoute(route, getClients); + const { statusCode } = await inject(getPatchRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if id is not given in either the body or the url', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { rule_id, ...noId } = typicalPayload(); const request: ServerInjectOptions = { method: 'PATCH', @@ -79,10 +83,10 @@ describe('patch_rules', () => { }); test('returns 404 if the record does not exist yet', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const request: ServerInjectOptions = { method: 'PATCH', url: DETECTION_ENGINE_RULES_URL, @@ -93,11 +97,11 @@ describe('patch_rules', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const request: ServerInjectOptions = { method: 'PATCH', url: DETECTION_ENGINE_RULES_URL, @@ -108,11 +112,11 @@ describe('patch_rules', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'PATCH', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts index e27ae81362f27..761d22b084237 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -5,18 +5,18 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { patchRules } from '../../rules/patch_rules'; import { PatchRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { patchRulesSchema } from '../schemas/patch_rules_schema'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { getIdError, transform } from './utils'; import { transformError } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createPatchRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createPatchRulesRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { method: 'PATCH', path: DETECTION_ENGINE_RULES_URL, @@ -59,21 +59,16 @@ export const createPatchRulesRoute = (server: ServerFacade): Hapi.ServerRoute => version, } = request.payload; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } - try { + const { alertsClient, actionsClient, savedObjectsClient } = await getClients(request); + + if (!actionsClient || !alertsClient) { + return headers.response().code(404); + } + const rule = await patchRules({ - alertsClient, actionsClient, + alertsClient, description, enabled, falsePositives, @@ -146,6 +141,6 @@ export const createPatchRulesRoute = (server: ServerFacade): Hapi.ServerRoute => }; }; -export const patchRulesRoute = (server: ServerFacade) => { - server.route(createPatchRulesRoute(server)); +export const patchRulesRoute = (route: LegacyServices['route'], getClients: GetScopedClients) => { + route(createPatchRulesRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts index 000cd29af8ba9..7c4653af97f21 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { readRulesRoute } from './read_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { readRulesRoute } from './read_rules_route'; import { getFindResult, getResult, @@ -19,43 +16,48 @@ import { getFindResultWithSingleHit, getFindResultStatus, } from '../__mocks__/request_responses'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; describe('read_signals', () => { - let { server, alertsClient, savedObjectsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { - ({ server, alertsClient, savedObjectsClient } = createMockServer()); - readRulesRoute(server); - }); - - afterEach(() => { jest.resetAllMocks(); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + readRulesRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when reading a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getReadRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - readRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getReadRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + readRulesRoute(route, getClients); + const { statusCode } = await inject(getReadRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if given a non-existent id', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const request: ServerInjectOptions = { method: 'GET', url: DETECTION_ENGINE_RULES_URL, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts index e82ad92704695..0180b208d1bb7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -5,18 +5,18 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { getIdError, transform } from './utils'; import { transformError } from '../utils'; import { readRules } from '../../rules/read_rules'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; import { queryRulesSchema } from '../schemas/query_rules_schema'; import { QueryRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { GetScopedClients } from '../../../../services'; -export const createReadRulesRoute: Hapi.ServerRoute = { +export const createReadRulesRoute = (getClients: GetScopedClients): Hapi.ServerRoute => ({ method: 'GET', path: DETECTION_ENGINE_RULES_URL, options: { @@ -28,16 +28,15 @@ export const createReadRulesRoute: Hapi.ServerRoute = { query: queryRulesSchema, }, }, - async handler(request: QueryRequest, headers) { + async handler(request: QueryRequest & LegacyRequest, headers) { const { id, rule_id: ruleId } = request.query; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } + try { + const { alertsClient, savedObjectsClient } = await getClients(request); + if (!alertsClient) { + return headers.response().code(404); + } + const rule = await readRules({ alertsClient, id, @@ -84,8 +83,8 @@ export const createReadRulesRoute: Hapi.ServerRoute = { .code(error.statusCode); } }, -}; +}); -export const readRulesRoute = (server: ServerFacade) => { - server.route(createReadRulesRoute); +export const readRulesRoute = (route: LegacyServices['route'], getClients: GetScopedClients) => { + route(createReadRulesRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts index 81b6444f38603..9ff7ebc37aab1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts @@ -4,14 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { updateRulesRoute } from './update_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; +import { updateRulesRoute } from './update_rules_route'; import { getFindResult, getResult, @@ -20,43 +16,52 @@ import { getFindResultWithSingleHit, getUpdateBulkRequest, } from '../__mocks__/request_responses'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { updateRulesBulkRoute } from './update_rules_bulk_route'; import { BulkError } from '../utils'; describe('update_rules_bulk', () => { - let { server, alertsClient, actionsClient } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient } = createMockServer()); - updateRulesBulkRoute(server); + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + updateRulesBulkRoute(server.route, config, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getUpdateBulkRequest()); expect(statusCode).toBe(200); }); test('returns 200 as a response when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getUpdateBulkRequest()); expect(statusCode).toBe(200); }); test('returns 404 within the payload when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { payload } = await server.inject(getUpdateBulkRequest()); const parsed: BulkError[] = JSON.parse(payload); const expected: BulkError[] = [ @@ -69,17 +74,18 @@ describe('update_rules_bulk', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - updateRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getUpdateBulkRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + updateRulesRoute(route, config, getClients); + const { statusCode } = await inject(getUpdateBulkRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if id is not given in either the body or the url', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); const { rule_id, ...noId } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', @@ -91,9 +97,9 @@ describe('update_rules_bulk', () => { }); test('returns errors as 200 to just indicate ok something happened', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PUT', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -104,9 +110,9 @@ describe('update_rules_bulk', () => { }); test('returns 404 in the payload if the record does not exist yet', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PUT', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -124,10 +130,10 @@ describe('update_rules_bulk', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PUT', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -138,10 +144,10 @@ describe('update_rules_bulk', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 671497f9f65db..98ed01474c3dc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -5,21 +5,24 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { BulkUpdateRulesRequest, IRuleSavedAttributesSavedObjectAttributes, } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { transformOrBulkError, getIdBulkError } from './utils'; import { transformBulkError, getIndex } from '../utils'; import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; import { updateRules } from '../../rules/update_rules'; -export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createUpdateRulesBulkRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'PUT', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -33,14 +36,11 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }, }, async handler(request: BulkUpdateRulesRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) + const { actionsClient, alertsClient, savedObjectsClient, spacesClient } = await getClients( + request ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + + if (!actionsClient || !alertsClient) { return headers.response().code(404); } @@ -74,7 +74,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou references, version, } = payloadRule; - const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); + const finalIndex = outputIndex ?? getIndex(spacesClient.getSpaceId, config); const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; try { const rule = await updateRules({ @@ -134,6 +134,10 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }; }; -export const updateRulesBulkRoute = (server: ServerFacade): void => { - server.route(createUpdateRulesBulkRoute(server)); +export const updateRulesBulkRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createUpdateRulesBulkRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index c4f10d7a20327..7cadfa94467a7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -4,14 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { updateRulesRoute } from './update_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; +import { updateRulesRoute } from './update_rules_route'; import { getFindResult, getFindResultStatus, @@ -21,51 +17,62 @@ import { typicalPayload, getFindResultWithSingleHit, } from '../__mocks__/request_responses'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('update_rules', () => { - let { server, alertsClient, actionsClient, savedObjectsClient } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient, savedObjectsClient } = createMockServer()); - updateRulesRoute(server); + + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + updateRulesRoute(server.route, config, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getUpdateRequest()); expect(statusCode).toBe(200); }); test('returns 404 when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - updateRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getUpdateRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + updateRulesRoute(route, config, getClients); + const { statusCode } = await inject(getUpdateRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if id is not given in either the body or the url', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { rule_id, ...noId } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', @@ -79,10 +86,10 @@ describe('update_rules', () => { }); test('returns 404 if the record does not exist yet', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const request: ServerInjectOptions = { method: 'PUT', url: DETECTION_ENGINE_RULES_URL, @@ -93,11 +100,11 @@ describe('update_rules', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const request: ServerInjectOptions = { method: 'PUT', url: DETECTION_ENGINE_RULES_URL, @@ -108,11 +115,11 @@ describe('update_rules', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index a01627d2094b7..80fdfc1df8e0e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -5,18 +5,20 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { UpdateRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { updateRulesSchema } from '../schemas/update_rules_schema'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { getIdError, transform } from './utils'; import { transformError, getIndex } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; import { updateRules } from '../../rules/update_rules'; -export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createUpdateRulesRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'PUT', path: DETECTION_ENGINE_RULES_URL, @@ -59,19 +61,16 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = version, } = request.payload; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } - try { - const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); + const { alertsClient, actionsClient, savedObjectsClient, spacesClient } = await getClients( + request + ); + + if (!actionsClient || !alertsClient) { + return headers.response().code(404); + } + + const finalIndex = outputIndex ?? getIndex(spacesClient.getSpaceId, config); const rule = await updateRules({ alertsClient, actionsClient, @@ -148,6 +147,10 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const updateRulesRoute = (server: ServerFacade) => { - server.route(createUpdateRulesRoute(server)); +export const updateRulesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(createUpdateRulesRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts index 35e1e5933af64..3e7ed4de6d8c6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMockServer } from '../__mocks__/_mock_server'; +import { ServerInjectOptions } from 'hapi'; +import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; import { setSignalsStatusRoute } from './open_close_signals_route'; import * as myUtils from '../utils'; -import { ServerInjectOptions } from 'hapi'; import { getSetSignalStatusByIdsRequest, @@ -16,19 +16,27 @@ import { typicalSetStatusSignalByQueryPayload, setStatusSignalMissingIdsAndQueryPayload, } from '../__mocks__/request_responses'; -import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; describe('set signal status', () => { - let { server, elasticsearch } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); jest.spyOn(myUtils, 'getIndex').mockReturnValue('fakeindex'); - ({ server, elasticsearch } = createMockServer()); - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn(() => true), - })); - setSignalsStatusRoute(server); + + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(true); + + setSignalsStatusRoute(server.route, config, getClients); }); describe('status on signal', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index 4755869c3d908..b2b938625180e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -6,12 +6,16 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { SignalsStatusRequest } from '../../signals/types'; import { setSignalsStatusSchema } from '../schemas/set_signal_status_schema'; -import { ServerFacade } from '../../../../types'; import { transformError, getIndex } from '../utils'; -export const setSignalsStatusRouteDef = (server: ServerFacade): Hapi.ServerRoute => { +export const setSignalsStatusRouteDef = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: DETECTION_ENGINE_SIGNALS_STATUS_URL, @@ -26,8 +30,9 @@ export const setSignalsStatusRouteDef = (server: ServerFacade): Hapi.ServerRoute }, async handler(request: SignalsStatusRequest) { const { signal_ids: signalIds, query, status } = request.payload; - const index = getIndex(request, server); - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); + const { clusterClient, spacesClient } = await getClients(request); + const index = getIndex(spacesClient.getSpaceId, config); + let queryObject; if (signalIds) { queryObject = { ids: { values: signalIds } }; @@ -40,7 +45,7 @@ export const setSignalsStatusRouteDef = (server: ServerFacade): Hapi.ServerRoute }; } try { - return callWithRequest(request, 'updateByQuery', { + return clusterClient.callAsCurrentUser('updateByQuery', { index, body: { script: { @@ -58,6 +63,10 @@ export const setSignalsStatusRouteDef = (server: ServerFacade): Hapi.ServerRoute }; }; -export const setSignalsStatusRoute = (server: ServerFacade) => { - server.route(setSignalsStatusRouteDef(server)); +export const setSignalsStatusRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(setSignalsStatusRouteDef(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts index 5b86d0a4b36c0..9439adfcec3cb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts @@ -4,77 +4,78 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMockServer } from '../__mocks__/_mock_server'; -import { querySignalsRoute } from './query_signals_route'; -import * as myUtils from '../utils'; import { ServerInjectOptions } from 'hapi'; +import { querySignalsRoute } from './query_signals_route'; +import * as myUtils from '../utils'; import { getSignalsQueryRequest, getSignalsAggsQueryRequest, typicalSignalsQuery, typicalSignalsQueryAggs, } from '../__mocks__/request_responses'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; describe('query for signal', () => { - let { server, elasticsearch } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); jest.spyOn(myUtils, 'getIndex').mockReturnValue('fakeindex'); - ({ server, elasticsearch } = createMockServer()); - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn(() => true), - })); - querySignalsRoute(server); + + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(true); + + querySignalsRoute(server.route, config, getClients); }); describe('query and agg on signals index', () => { test('returns 200 when using single query', async () => { - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn( - (_req, _type: string, queryBody: { index: string; body: object }) => { - expect(queryBody.body).toMatchObject({ ...typicalSignalsQueryAggs() }); - return true; - } - ), - })); - const { statusCode } = await server.inject(getSignalsAggsQueryRequest()); + const { statusCode } = await server.inject(getSignalsQueryRequest()); + expect(statusCode).toBe(200); + expect(clients.clusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'search', + expect.objectContaining({ body: typicalSignalsQuery() }) + ); expect(myUtils.getIndex).toHaveReturnedWith('fakeindex'); }); test('returns 200 when using single agg', async () => { - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn( - (_req, _type: string, queryBody: { index: string; body: object }) => { - expect(queryBody.body).toMatchObject({ ...typicalSignalsQueryAggs() }); - return true; - } - ), - })); const { statusCode } = await server.inject(getSignalsAggsQueryRequest()); + expect(statusCode).toBe(200); + expect(clients.clusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'search', + expect.objectContaining({ body: typicalSignalsQueryAggs() }) + ); expect(myUtils.getIndex).toHaveReturnedWith('fakeindex'); }); test('returns 200 when using aggs and query together', async () => { - const allTogether = getSignalsQueryRequest(); - allTogether.payload = { ...typicalSignalsQueryAggs(), ...typicalSignalsQuery() }; - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn( - (_req, _type: string, queryBody: { index: string; body: object }) => { - expect(queryBody.body).toMatchObject({ - ...typicalSignalsQueryAggs(), - ...typicalSignalsQuery(), - }); - return true; - } - ), - })); - const { statusCode } = await server.inject(allTogether); + const request = getSignalsQueryRequest(); + request.payload = { ...typicalSignalsQueryAggs(), ...typicalSignalsQuery() }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + expect(clients.clusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'search', + expect.objectContaining({ + body: { + ...typicalSignalsQuery(), + ...typicalSignalsQueryAggs(), + }, + }) + ); expect(myUtils.getIndex).toHaveReturnedWith('fakeindex'); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts index 6d1896b1a8171..a3602ffbded41 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -6,12 +6,16 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { SignalsQueryRequest } from '../../signals/types'; import { querySignalsSchema } from '../schemas/query_signals_index_schema'; -import { ServerFacade } from '../../../../types'; import { transformError, getIndex } from '../utils'; -export const querySignalsRouteDef = (server: ServerFacade): Hapi.ServerRoute => { +export const querySignalsRouteDef = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: DETECTION_ENGINE_QUERY_SIGNALS_URL, @@ -26,11 +30,12 @@ export const querySignalsRouteDef = (server: ServerFacade): Hapi.ServerRoute => }, async handler(request: SignalsQueryRequest) { const { query, aggs, _source, track_total_hits, size } = request.payload; - const index = getIndex(request, server); - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); + const { clusterClient, spacesClient } = await getClients(request); + + const index = getIndex(spacesClient.getSpaceId, config); try { - return callWithRequest(request, 'search', { + return clusterClient.callAsCurrentUser('search', { index, body: { query, aggs, _source, track_total_hits, size }, }); @@ -42,6 +47,10 @@ export const querySignalsRouteDef = (server: ServerFacade): Hapi.ServerRoute => }; }; -export const querySignalsRoute = (server: ServerFacade) => { - server.route(querySignalsRouteDef(server)); +export const querySignalsRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(querySignalsRouteDef(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts index f6d297b0cbf43..b17be21d15a19 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts @@ -5,13 +5,14 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; + import { DETECTION_ENGINE_TAGS_URL } from '../../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; import { transformError } from '../utils'; import { readTags } from '../../tags/read_tags'; +import { GetScopedClients } from '../../../../services'; -export const createReadTagsRoute: Hapi.ServerRoute = { +export const createReadTagsRoute = (getClients: GetScopedClients): Hapi.ServerRoute => ({ method: 'GET', path: DETECTION_ENGINE_TAGS_URL, options: { @@ -22,8 +23,9 @@ export const createReadTagsRoute: Hapi.ServerRoute = { }, }, }, - async handler(request: RequestFacade, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + async handler(request: LegacyRequest, headers) { + const { alertsClient } = await getClients(request); + if (!alertsClient) { return headers.response().code(404); } @@ -43,8 +45,8 @@ export const createReadTagsRoute: Hapi.ServerRoute = { .code(error.statusCode); } }, -}; +}); -export const readTagsRoute = (server: ServerFacade) => { - server.route(createReadTagsRoute); +export const readTagsRoute = (route: LegacyServices['route'], getClients: GetScopedClients) => { + route(createReadTagsRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 2699f687c5106..957ddd4ee6caa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -16,6 +16,7 @@ import { createImportErrorObject, transformImportError, } from './utils'; +import { createMockConfig } from './__mocks__'; describe('utils', () => { describe('transformError', () => { @@ -295,34 +296,20 @@ describe('utils', () => { }); describe('getIndex', () => { - it('appends the space ID to the configured index if spaces are enabled', () => { - const mockGet = jest.fn(); - const mockGetSpaceId = jest.fn(); - const config = jest.fn(() => ({ get: mockGet, has: jest.fn() })); - const server = { plugins: { spaces: { getSpaceId: mockGetSpaceId } }, config }; + let mockConfig = createMockConfig(); - mockGet.mockReturnValue('mockSignalsIndex'); - mockGetSpaceId.mockReturnValue('myspace'); - // @ts-ignore-next-line TODO these dependencies are simplified on - // https://github.com/elastic/kibana/pull/56814. We're currently mocking - // out what we need. - const index = getIndex(null, server); - - expect(index).toEqual('mockSignalsIndex-myspace'); + beforeEach(() => { + mockConfig = () => ({ + get: jest.fn(() => 'mockSignalsIndex'), + has: jest.fn(), + }); }); - it('appends the default space ID to the configured index if spaces are disabled', () => { - const mockGet = jest.fn(); - const config = jest.fn(() => ({ get: mockGet, has: jest.fn() })); - const server = { plugins: {}, config }; + it('appends the space id to the configured index', () => { + const getSpaceId = jest.fn(() => 'myspace'); + const index = getIndex(getSpaceId, mockConfig); - mockGet.mockReturnValue('mockSignalsIndex'); - // @ts-ignore-next-line TODO these dependencies are simplified on - // https://github.com/elastic/kibana/pull/56814. We're currently mocking - // out what we need. - const index = getIndex(null, server); - - expect(index).toEqual('mockSignalsIndex-default'); + expect(index).toEqual('mockSignalsIndex-myspace'); }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 20871e5309c30..4a586e21c9e7f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../types'; +import { LegacyServices } from '../../../types'; export interface OutputError { message: string; @@ -174,21 +174,9 @@ export const transformBulkError = ( } }; -export const getIndex = ( - request: RequestFacade | Omit, - server: ServerFacade -): string => { - const spaceId = server.plugins.spaces?.getSpaceId?.(request) ?? 'default'; - const signalsIndex = server.config().get(`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`); - return `${signalsIndex}-${spaceId}`; -}; +export const getIndex = (getSpaceId: () => string, config: LegacyServices['config']): string => { + const signalsIndex = config().get(`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`); + const spaceId = getSpaceId(); -export const callWithRequestFactory = ( - request: RequestFacade | Omit, - server: ServerFacade -) => { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); - return (endpoint: string, params: T, options?: U) => { - return callWithRequest(request, endpoint, params, options); - }; + return `${signalsIndex}-${spaceId}`; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts index 8d00ddb18be6b..25bac383ecf72 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts @@ -5,7 +5,6 @@ */ import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; -import { AlertsClient } from '../../../../../alerting'; import { getResult, getFindResultWithSingleHit, @@ -28,10 +27,7 @@ describe('get_existing_prepackaged_rules', () => { test('should return a single item in a single page', async () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getExistingPrepackagedRules({ - alertsClient: unsafeCast, - }); + const rules = await getExistingPrepackagedRules({ alertsClient }); expect(rules).toEqual([getResult()]); }); @@ -70,10 +66,7 @@ describe('get_existing_prepackaged_rules', () => { }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getExistingPrepackagedRules({ - alertsClient: unsafeCast, - }); + const rules = await getExistingPrepackagedRules({ alertsClient }); expect(rules).toEqual([result1, result2, result3]); }); }); @@ -82,10 +75,7 @@ describe('get_existing_prepackaged_rules', () => { test('should return a single item in a single page', async () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getNonPackagedRules({ - alertsClient: unsafeCast, - }); + const rules = await getNonPackagedRules({ alertsClient }); expect(rules).toEqual([getResult()]); }); @@ -113,10 +103,7 @@ describe('get_existing_prepackaged_rules', () => { getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getNonPackagedRules({ - alertsClient: unsafeCast, - }); + const rules = await getNonPackagedRules({ alertsClient }); expect(rules).toEqual([result1, result2]); }); @@ -152,10 +139,7 @@ describe('get_existing_prepackaged_rules', () => { }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getNonPackagedRules({ - alertsClient: unsafeCast, - }); + const rules = await getNonPackagedRules({ alertsClient }); expect(rules).toEqual([result1, result2, result3]); }); }); @@ -164,11 +148,7 @@ describe('get_existing_prepackaged_rules', () => { test('should return a single item in a single page', async () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRules({ - alertsClient: unsafeCast, - filter: '', - }); + const rules = await getRules({ alertsClient, filter: '' }); expect(rules).toEqual([getResult()]); }); @@ -196,11 +176,7 @@ describe('get_existing_prepackaged_rules', () => { getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRules({ - alertsClient: unsafeCast, - filter: '', - }); + const rules = await getRules({ alertsClient, filter: '' }); expect(rules).toEqual([result1, result2]); }); }); @@ -209,11 +185,7 @@ describe('get_existing_prepackaged_rules', () => { test('it returns a count', async () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRulesCount({ - alertsClient: unsafeCast, - filter: '', - }); + const rules = await getRulesCount({ alertsClient, filter: '' }); expect(rules).toEqual(1); }); }); @@ -222,10 +194,7 @@ describe('get_existing_prepackaged_rules', () => { test('it returns a count', async () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getNonPackagedRulesCount({ - alertsClient: unsafeCast, - }); + const rules = await getNonPackagedRulesCount({ alertsClient }); expect(rules).toEqual(1); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts index ff48b9f5f7c33..35d3489dad6fc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts @@ -10,7 +10,6 @@ import { getFindResultWithSingleHit, FindHit, } from '../routes/__mocks__/request_responses'; -import { AlertsClient } from '../../../../../alerting'; import { getExportAll } from './get_export_all'; describe('getExportAll', () => { @@ -19,8 +18,7 @@ describe('getExportAll', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const exports = await getExportAll(unsafeCast); + const exports = await getExportAll(alertsClient); expect(exports).toEqual({ rulesNdjson: '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"saved_id":"some-id","timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"version":1}\n', @@ -39,8 +37,7 @@ describe('getExportAll', () => { alertsClient.find.mockResolvedValue(findResult); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const exports = await getExportAll(unsafeCast); + const exports = await getExportAll(alertsClient); expect(exports).toEqual({ rulesNdjson: '', exportDetails: '{"exported_count":0,"missing_rules":[],"missing_rules_count":0}\n', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 236d04acc782b..4b6ea527a2027 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -11,7 +11,6 @@ import { getFindResultWithSingleHit, FindHit, } from '../routes/__mocks__/request_responses'; -import { AlertsClient } from '../../../../../alerting'; describe('get_export_by_object_ids', () => { describe('getExportByObjectIds', () => { @@ -20,9 +19,8 @@ describe('get_export_by_object_ids', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const objects = [{ rule_id: 'rule-1' }]; - const exports = await getExportByObjectIds(unsafeCast, objects); + const exports = await getExportByObjectIds(alertsClient, objects); expect(exports).toEqual({ rulesNdjson: '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"saved_id":"some-id","timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"version":1}\n', @@ -45,9 +43,8 @@ describe('get_export_by_object_ids', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(findResult); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const objects = [{ rule_id: 'rule-1' }]; - const exports = await getExportByObjectIds(unsafeCast, objects); + const exports = await getExportByObjectIds(alertsClient, objects); expect(exports).toEqual({ rulesNdjson: '', exportDetails: @@ -62,9 +59,8 @@ describe('get_export_by_object_ids', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(unsafeCast, objects); + const exports = await getRulesFromObjects(alertsClient, objects); const expected: RulesErrors = { exportedCount: 1, missingRules: [], @@ -138,9 +134,8 @@ describe('get_export_by_object_ids', () => { alertsClient.get.mockResolvedValue(result); alertsClient.find.mockResolvedValue(findResult); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(unsafeCast, objects); + const exports = await getRulesFromObjects(alertsClient, objects); const expected: RulesErrors = { exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], @@ -162,9 +157,8 @@ describe('get_export_by_object_ids', () => { alertsClient.get.mockRejectedValue({ output: { statusCode: 404 } }); alertsClient.find.mockResolvedValue(findResult); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(unsafeCast, objects); + const exports = await getRulesFromObjects(alertsClient, objects); const expected: RulesErrors = { exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts index 6ba0aa95bdd7b..c637860c5646a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts @@ -6,7 +6,6 @@ import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; import { readRules } from './read_rules'; -import { AlertsClient } from '../../../../../alerting'; import { getResult, getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; describe('read_rules', () => { @@ -15,9 +14,8 @@ describe('read_rules', () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: undefined, }); @@ -28,9 +26,8 @@ describe('read_rules', () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: null, }); @@ -42,9 +39,8 @@ describe('read_rules', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: undefined, ruleId: 'rule-1', }); @@ -56,9 +52,8 @@ describe('read_rules', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: null, ruleId: 'rule-1', }); @@ -70,9 +65,8 @@ describe('read_rules', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: null, ruleId: null, }); @@ -84,9 +78,8 @@ describe('read_rules', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: undefined, ruleId: undefined, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index 8c44d82f46b53..8579447a74c69 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -14,10 +14,10 @@ import { SavedObjectsClientContract, } from 'kibana/server'; import { SIGNALS_ID } from '../../../../common/constants'; +import { LegacyRequest } from '../../../types'; import { AlertsClient } from '../../../../../alerting/server'; import { ActionsClient } from '../../../../../../../plugins/actions/server'; import { RuleAlertParams, RuleTypeParams, RuleAlertParamsRest } from '../types'; -import { RequestFacade } from '../../../types'; import { Alert } from '../../../../../alerting/server/types'; export type PatchRuleAlertParamsRest = Partial & { @@ -39,19 +39,19 @@ export interface FindParamsRest { filter: string; } -export interface PatchRulesRequest extends RequestFacade { +export interface PatchRulesRequest extends LegacyRequest { payload: PatchRuleAlertParamsRest; } -export interface BulkPatchRulesRequest extends RequestFacade { +export interface BulkPatchRulesRequest extends LegacyRequest { payload: PatchRuleAlertParamsRest[]; } -export interface UpdateRulesRequest extends RequestFacade { +export interface UpdateRulesRequest extends LegacyRequest { payload: UpdateRuleAlertParamsRest; } -export interface BulkUpdateRulesRequest extends RequestFacade { +export interface BulkUpdateRulesRequest extends LegacyRequest { payload: UpdateRuleAlertParamsRest[]; } @@ -99,11 +99,11 @@ export interface IRuleStatusFindType { export type RuleStatusString = 'succeeded' | 'failed' | 'going to run' | 'executing'; -export interface RulesRequest extends RequestFacade { +export interface RulesRequest extends LegacyRequest { payload: RuleAlertParamsRest; } -export interface BulkRulesRequest extends RequestFacade { +export interface BulkRulesRequest extends LegacyRequest { payload: RuleAlertParamsRest[]; } @@ -112,12 +112,12 @@ export interface HapiReadableStream extends Readable { filename: string; }; } -export interface ImportRulesRequest extends Omit { +export interface ImportRulesRequest extends Omit { query: { overwrite: boolean }; payload: { file: HapiReadableStream }; } -export interface ExportRulesRequest extends Omit { +export interface ExportRulesRequest extends Omit { payload: { objects: Array<{ rule_id: string }> | null | undefined }; query: { file_name: string; @@ -125,11 +125,11 @@ export interface ExportRulesRequest extends Omit { }; } -export type QueryRequest = Omit & { +export type QueryRequest = Omit & { query: { id: string | undefined; rule_id: string | undefined }; }; -export interface QueryBulkRequest extends RequestFacade { +export interface QueryBulkRequest extends LegacyRequest { payload: Array; } @@ -143,7 +143,7 @@ export interface FindRuleParams { sortOrder?: 'asc' | 'desc'; } -export interface FindRulesRequest extends Omit { +export interface FindRulesRequest extends Omit { query: { per_page: number; page: number; @@ -155,7 +155,7 @@ export interface FindRulesRequest extends Omit { }; } -export interface FindRulesStatusesRequest extends Omit { +export interface FindRulesStatusesRequest extends Omit { query: { ids: string[]; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts index 9b7b2b8f1fff9..e9159ab87a0c0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -6,7 +6,7 @@ import { RuleAlertParams, OutputRuleAlertRest } from '../types'; import { SearchResponse } from '../../types'; -import { RequestFacade } from '../../../types'; +import { LegacyRequest } from '../../../types'; import { AlertType, State, AlertExecutorOptions } from '../../../../../alerting/server/types'; export interface SignalsParams { @@ -35,11 +35,11 @@ export type SignalsStatusRestParams = Omit & { export type SignalsQueryRestParams = SignalQueryParams; -export interface SignalsStatusRequest extends RequestFacade { +export interface SignalsStatusRequest extends LegacyRequest { payload: SignalsStatusRestParams; } -export interface SignalsQueryRequest extends RequestFacade { +export interface SignalsQueryRequest extends LegacyRequest { payload: SignalsQueryRestParams; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts index 87739bf785012..940011924de79 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts @@ -5,7 +5,6 @@ */ import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; -import { AlertsClient } from '../../../../../alerting'; import { getResult, getFindResultWithMultiHits } from '../routes/__mocks__/request_responses'; import { INTERNAL_RULE_ID_KEY, INTERNAL_IDENTIFIER } from '../../../../common/constants'; import { readRawTags, readTags, convertTagsToSet, convertToTags, isTags } from './read_tags'; @@ -30,10 +29,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); + const tags = await readRawTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); @@ -51,10 +47,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); + const tags = await readRawTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); @@ -72,10 +65,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); + const tags = await readRawTags({ alertsClient }); expect(tags).toEqual([]); }); @@ -88,10 +78,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); + const tags = await readRawTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2']); }); @@ -104,10 +91,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); + const tags = await readRawTags({ alertsClient }); expect(tags).toEqual([]); }); }); @@ -127,10 +111,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); @@ -148,10 +129,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); @@ -169,10 +147,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual([]); }); @@ -185,10 +160,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2']); }); @@ -201,10 +173,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual([]); }); @@ -221,10 +190,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual(['tag 1']); }); @@ -257,10 +223,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4', 'tag 5']); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index e15053db75777..08cb2e7bc19ee 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CallAPIOptions } from '../../../../../../../src/core/server'; import { Filter } from '../../../../../../../src/plugins/data/server'; import { IRuleStatusAttributes } from './rules/types'; @@ -117,4 +118,9 @@ export type PrepackagedRules = Omit< | 'created_at' > & { rule_id: string; immutable: boolean }; -export type CallWithRequest = (endpoint: string, params: T, options?: U) => Promise; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type CallWithRequest, V> = ( + endpoint: string, + params: T, + options?: CallAPIOptions +) => Promise; diff --git a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.test.ts index b1f0c3c4a3a18..42dc13d84fd98 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.test.ts @@ -519,7 +519,6 @@ describe('events elasticsearch_adapter', () => { return mockResponseMap; }); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/events/mock.ts b/x-pack/legacy/plugins/siem/server/lib/events/mock.ts index 195c0cd674af5..3eb841cbad411 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/mock.ts @@ -189,8 +189,7 @@ export const mockOptions: RequestDetailsOptions = { }; export const mockRequest = { - params: {}, - payload: { + body: { operationName: 'GetNetworkTopNFlowQuery', variables: { indexName: 'auditbeat-8.0.0-2019.03.29-000003', diff --git a/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts index 39f75e6ea36c3..4cce0b0999257 100644 --- a/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts @@ -9,35 +9,27 @@ import { GraphQLSchema } from 'graphql'; import { runHttpQuery } from 'apollo-server-core'; import { schema as configSchema } from '@kbn/config-schema'; import { - CoreSetup, IRouter, KibanaResponseFactory, RequestHandlerContext, - PluginInitializerContext, KibanaRequest, } from '../../../../../../../src/core/server'; import { IndexPatternsFetcher } from '../../../../../../../src/plugins/data/server'; import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; -import { RequestFacade } from '../../types'; +import { CoreSetup, SetupPlugins } from '../../plugin'; import { FrameworkAdapter, FrameworkIndexPatternsService, FrameworkRequest, internalFrameworkRequest, - WrappableRequest, } from './types'; -import { SiemPluginSecurity, PluginsSetup } from '../../plugin'; export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { - public version: string; - private isProductionMode: boolean; private router: IRouter; - private security: SiemPluginSecurity; + private security: SetupPlugins['security']; - constructor(core: CoreSetup, plugins: PluginsSetup, env: PluginInitializerContext['env']) { - this.version = env.packageInfo.version; - this.isProductionMode = env.mode.prod; + constructor(core: CoreSetup, plugins: SetupPlugins, private isProductionMode: boolean) { this.router = core.http.createRouter(); this.security = plugins.security; } @@ -68,13 +60,7 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { this.router.post( { path: routePath, - validate: { - body: configSchema.object({ - operationName: configSchema.string(), - query: configSchema.string(), - variables: configSchema.object({}, { allowUnknowns: true }), - }), - }, + validate: { body: configSchema.object({}, { allowUnknowns: true }) }, options: { tags: ['access:siem'], }, @@ -84,7 +70,7 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { const user = await this.getCurrentUserInfo(request); const gqlResponse = await runHttpQuery([request], { method: 'POST', - options: (req: RequestFacade) => ({ + options: (req: KibanaRequest) => ({ context: { req: wrapRequest(req, context, user) }, schema, }), @@ -104,39 +90,6 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { ); if (!this.isProductionMode) { - this.router.get( - { - path: routePath, - validate: { query: configSchema.object({}, { allowUnknowns: true }) }, - options: { - tags: ['access:siem'], - }, - }, - async (context, request, response) => { - try { - const user = await this.getCurrentUserInfo(request); - const { query } = request; - const gqlResponse = await runHttpQuery([request], { - method: 'GET', - options: (req: RequestFacade) => ({ - context: { req: wrapRequest(req, context, user) }, - schema, - }), - query, - }); - - return response.ok({ - body: gqlResponse, - headers: { - 'content-type': 'application/json', - }, - }); - } catch (error) { - return this.handleError(error, response); - } - } - ); - this.router.get( { path: `${routePath}/graphiql`, @@ -150,7 +103,7 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { request.query, { endpointURL: routePath, - passHeader: `'kbn-version': '${this.version}'`, + passHeader: "'kbn-xsrf': 'graphiql'", }, request ); @@ -208,20 +161,15 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { } } -export function wrapRequest( - req: InternalRequest, +export function wrapRequest( + request: KibanaRequest, context: RequestHandlerContext, user: AuthenticatedUser | null -): FrameworkRequest { - const { auth, params, payload, query } = req; - +): FrameworkRequest { return { - [internalFrameworkRequest]: req, - auth, + [internalFrameworkRequest]: request, + body: request.body, context, - params, - payload, - query, user, }; } diff --git a/x-pack/legacy/plugins/siem/server/lib/framework/types.ts b/x-pack/legacy/plugins/siem/server/lib/framework/types.ts index 67861ce0dcf28..9fc78e6fb84fe 100644 --- a/x-pack/legacy/plugins/siem/server/lib/framework/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/framework/types.ts @@ -6,9 +6,8 @@ import { IndicesGetMappingParams } from 'elasticsearch'; import { GraphQLSchema } from 'graphql'; -import { RequestAuth } from 'hapi'; -import { RequestHandlerContext } from '../../../../../../../src/core/server'; +import { RequestHandlerContext, KibanaRequest } from '../../../../../../../src/core/server'; import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; import { ESQuery } from '../../../common/typed_json'; import { @@ -19,14 +18,12 @@ import { TimerangeInput, Maybe, } from '../../graphql/types'; -import { RequestFacade } from '../../types'; export * from '../../utils/typed_resolvers'; export const internalFrameworkRequest = Symbol('internalFrameworkRequest'); export interface FrameworkAdapter { - version: string; registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void; callWithRequest( req: FrameworkRequest, @@ -46,24 +43,12 @@ export interface FrameworkAdapter { getIndexPatternsService(req: FrameworkRequest): FrameworkIndexPatternsService; } -export interface FrameworkRequest { - [internalFrameworkRequest]: InternalRequest; +export interface FrameworkRequest extends Pick { + [internalFrameworkRequest]: KibanaRequest; context: RequestHandlerContext; - payload: InternalRequest['payload']; - params: InternalRequest['params']; - query: InternalRequest['query']; - auth: InternalRequest['auth']; user: AuthenticatedUser | null; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface WrappableRequest { - payload: Payload; - params: Params; - query: Query; - auth: RequestAuth; -} - export interface DatabaseResponse { took: number; timeout: boolean; diff --git a/x-pack/legacy/plugins/siem/server/lib/hosts/elasticsearch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/hosts/elasticsearch_adapter.test.ts index 0d698f1e19213..20510e1089f96 100644 --- a/x-pack/legacy/plugins/siem/server/lib/hosts/elasticsearch_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/hosts/elasticsearch_adapter.test.ts @@ -159,7 +159,6 @@ describe('hosts elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockGetHostsResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -180,7 +179,6 @@ describe('hosts elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockGetHostOverviewResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -201,7 +199,6 @@ describe('hosts elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockGetHostLastFirstSeenResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/hosts/mock.ts b/x-pack/legacy/plugins/siem/server/lib/hosts/mock.ts index 66b73742cc45e..6b72c4a5a2843 100644 --- a/x-pack/legacy/plugins/siem/server/lib/hosts/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/hosts/mock.ts @@ -49,8 +49,7 @@ export const mockGetHostsOptions: HostsRequestOptions = { }; export const mockGetHostsRequest = { - params: {}, - payload: { + body: { operationName: 'GetHostsTableQuery', variables: { sourceId: 'default', @@ -67,7 +66,6 @@ export const mockGetHostsRequest = { query: 'query GetHostsTableQuery($sourceId: ID!, $timerange: TimerangeInput!, $pagination: PaginationInput!, $sort: HostsSortField!, $filterQuery: String) {\n source(id: $sourceId) {\n id\n Hosts(timerange: $timerange, pagination: $pagination, sort: $sort, filterQuery: $filterQuery) {\n totalCount\n edges {\n node {\n _id\n host {\n id\n name\n os {\n name\n version\n __typename\n }\n __typename\n }\n __typename\n }\n cursor {\n value\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor {\n value\n __typename\n }\n hasNextPage\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockGetHostsResponse = { @@ -327,14 +325,12 @@ export const mockGetHostOverviewOptions: HostOverviewRequestOptions = { }; export const mockGetHostOverviewRequest = { - params: {}, - payload: { + body: { operationName: 'GetHostOverviewQuery', variables: { sourceId: 'default', hostName: 'siem-es' }, query: 'query GetHostOverviewQuery($sourceId: ID!, $hostName: String!, $timerange: TimerangeInput!) {\n source(id: $sourceId) {\n id\n HostOverview(hostName: $hostName, timerange: $timerange) {\n _id\n host {\n architecture\n id\n ip\n mac\n name\n os {\n family\n name\n platform\n version\n __typename\n }\n type\n __typename\n }\n cloud {\n instance {\n id\n __typename\n }\n machine {\n type\n __typename\n }\n provider\n region\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockGetHostOverviewResponse = { @@ -520,14 +516,12 @@ export const mockGetHostLastFirstSeenOptions: HostLastFirstSeenRequestOptions = }; export const mockGetHostLastFirstSeenRequest = { - params: {}, - payload: { + body: { operationName: 'GetHostLastFirstSeenQuery', variables: { sourceId: 'default', hostName: 'siem-es' }, query: 'query GetHostLastFirstSeenQuery($sourceId: ID!, $hostName: String!) {\n source(id: $sourceId) {\n id\n HostLastFirstSeen(hostName: $hostName) {\n firstSeen\n lastSeen\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockGetHostLastFirstSeenResponse = { diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts index 4a179073852b0..059d15220b619 100644 --- a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts @@ -53,7 +53,6 @@ describe('getKpiHosts', () => { let data: KpiHostsData; const mockCallWithRequest = jest.fn(); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -167,7 +166,6 @@ describe('getKpiHostDetails', () => { let data: KpiHostDetailsData; const mockCallWithRequest = jest.fn(); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/mock.ts b/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/mock.ts index a1962067f9bec..b82a540900bd0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/mock.ts @@ -43,8 +43,7 @@ export const mockKpiHostDetailsOptions: RequestBasicOptions = { }; export const mockKpiHostsRequest = { - params: {}, - payload: { + body: { operationName: 'GetKpiHostsQuery', variables: { sourceId: 'default', @@ -54,12 +53,10 @@ export const mockKpiHostsRequest = { query: 'fragment KpiHostChartFields on KpiHostHistogramData {\n x\n y\n __typename\n}\n\nquery GetKpiHostsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String, $defaultIndex: [String!]!) {\n source(id: $sourceId) {\n id\n KpiHosts(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex) {\n hosts\n hostsHistogram {\n ...KpiHostChartFields\n __typename\n }\n authSuccess\n authSuccessHistogram {\n ...KpiHostChartFields\n __typename\n }\n authFailure\n authFailureHistogram {\n ...KpiHostChartFields\n __typename\n }\n uniqueSourceIps\n uniqueSourceIpsHistogram {\n ...KpiHostChartFields\n __typename\n }\n uniqueDestinationIps\n uniqueDestinationIpsHistogram {\n ...KpiHostChartFields\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockKpiHostDetailsRequest = { - params: {}, - payload: { + body: { operationName: 'GetKpiHostDetailsQuery', variables: { sourceId: 'default', @@ -69,7 +66,6 @@ export const mockKpiHostDetailsRequest = { query: 'fragment KpiHostDetailsChartFields on KpiHostHistogramData {\n x\n y\n __typename\n}\n\nquery GetKpiHostDetailsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String, $defaultIndex: [String!]!, $hostName: String!) {\n source(id: $sourceId) {\n id\n KpiHostDetails(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex, hostName: $hostName) {\n authSuccess\n authSuccessHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n authFailure\n authFailureHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n uniqueSourceIps\n uniqueSourceIpsHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n uniqueDestinationIps\n uniqueDestinationIpsHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; const mockUniqueIpsResponse = { diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/elastic_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/kpi_network/elastic_adapter.test.ts index 11d007f591fac..58ee7c9aa1cf8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/kpi_network/elastic_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/kpi_network/elastic_adapter.test.ts @@ -48,7 +48,6 @@ describe('Network Kpi elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/mock.ts b/x-pack/legacy/plugins/siem/server/lib/kpi_network/mock.ts index 5b0601b88c779..7d86769de09f1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/kpi_network/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/kpi_network/mock.ts @@ -24,8 +24,7 @@ export const mockOptions: RequestBasicOptions = { }; export const mockRequest = { - params: {}, - payload: { + body: { operationName: 'GetKpiNetworkQuery', variables: { sourceId: 'default', @@ -35,7 +34,6 @@ export const mockRequest = { query: 'fragment KpiNetworkChartFields on KpiNetworkHistogramData {\n x\n y\n __typename\n}\n\nquery GetKpiNetworkQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String, $defaultIndex: [String!]!) {\n source(id: $sourceId) {\n id\n KpiNetwork(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex) {\n networkEvents\n uniqueFlowId\n uniqueSourcePrivateIps\n uniqueSourcePrivateIpsHistogram {\n ...KpiNetworkChartFields\n __typename\n }\n uniqueDestinationPrivateIps\n uniqueDestinationPrivateIpsHistogram {\n ...KpiNetworkChartFields\n __typename\n }\n dnsQueries\n tlsHandshakes\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockResponse = { diff --git a/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts index eeea4bec2fb25..eab461ee07ca7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts @@ -35,7 +35,6 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () = const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, getIndexPatternsService: jest.fn(), registerGraphQLEndpoint: jest.fn(), @@ -61,7 +60,6 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () = const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockNoDataResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -101,7 +99,6 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () = ].buckets[0].location.top_geo.hits.hits = []; mockCallWithRequest.mockResolvedValue(mockNoGeoDataResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, getIndexPatternsService: jest.fn(), registerGraphQLEndpoint: jest.fn(), @@ -132,7 +129,6 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () = const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockNoPaginationResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -155,7 +151,6 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () = const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockResponseIp); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, getIndexPatternsService: jest.fn(), registerGraphQLEndpoint: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/network/mock.ts b/x-pack/legacy/plugins/siem/server/lib/network/mock.ts index 21b00bf188d20..7ea692f27ef04 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/mock.ts @@ -59,8 +59,7 @@ export const mockOptions: NetworkTopNFlowRequestOptions = { }; export const mockRequest = { - params: {}, - payload: { + body: { operationName: 'GetNetworkTopNFlowQuery', variables: { filterQuery: '', @@ -1507,10 +1506,10 @@ export const mockOptionsIp: NetworkTopNFlowRequestOptions = { export const mockRequestIp = { ...mockRequest, - payload: { - ...mockRequest.payload, + body: { + ...mockRequest.body, variables: { - ...mockRequest.payload.variables, + ...mockRequest.body.variables, ip: '1.1.1.1', }, }, diff --git a/x-pack/legacy/plugins/siem/server/lib/overview/elastic_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/overview/elastic_adapter.test.ts index 29035f4539be8..f421704dffe12 100644 --- a/x-pack/legacy/plugins/siem/server/lib/overview/elastic_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/overview/elastic_adapter.test.ts @@ -36,7 +36,6 @@ describe('Siem Overview elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockResponseNetwork); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -70,7 +69,6 @@ describe('Siem Overview elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockNoDataResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -108,7 +106,6 @@ describe('Siem Overview elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockResponseHost); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -148,7 +145,6 @@ describe('Siem Overview elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockNoDataResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/overview/mock.ts b/x-pack/legacy/plugins/siem/server/lib/overview/mock.ts index 6196f45029313..410b4d90b1e78 100644 --- a/x-pack/legacy/plugins/siem/server/lib/overview/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/overview/mock.ts @@ -24,8 +24,7 @@ export const mockOptionsNetwork: RequestBasicOptions = { }; export const mockRequestNetwork = { - params: {}, - payload: { + body: { operationName: 'GetOverviewNetworkQuery', variables: { sourceId: 'default', @@ -35,7 +34,6 @@ export const mockRequestNetwork = { query: 'query GetOverviewNetworkQuery(\n $sourceId: ID!\n $timerange: TimerangeInput!\n $filterQuery: String\n ) {\n source(id: $sourceId) {\n id\n OverviewNetwork(timerange: $timerange, filterQuery: $filterQuery) {\n packetbeatFlow\n packetbeatDNS\n filebeatSuricata\n filebeatZeek\n auditbeatSocket\n }\n }\n }', }, - query: {}, }; export const mockResponseNetwork = { @@ -97,8 +95,7 @@ export const mockOptionsHost: RequestBasicOptions = { }; export const mockRequestHost = { - params: {}, - payload: { + body: { operationName: 'GetOverviewHostQuery', variables: { sourceId: 'default', @@ -108,7 +105,6 @@ export const mockRequestHost = { query: 'query GetOverviewHostQuery(\n $sourceId: ID!\n $timerange: TimerangeInput!\n $filterQuery: String\n ) {\n source(id: $sourceId) {\n id\n OverviewHost(timerange: $timerange, filterQuery: $filterQuery) {\n auditbeatAuditd\n auditbeatFIM\n auditbeatLogin\n auditbeatPackage\n auditbeatProcess\n auditbeatUser\n }\n }\n }', }, - query: {}, }; export const mockResponseHost = { diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts index 32a5c72215dda..428685cbaddb8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts @@ -22,7 +22,6 @@ describe('elasticsearch_adapter', () => { let data: TlsData; const mockCallWithRequest = jest.fn(); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/mock.ts b/x-pack/legacy/plugins/siem/server/lib/tls/mock.ts index a81862b6e7e90..4b27d541ec992 100644 --- a/x-pack/legacy/plugins/siem/server/lib/tls/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/tls/mock.ts @@ -212,8 +212,7 @@ export const expectedTlsEdges = [ ]; export const mockRequest = { - params: {}, - payload: { + body: { operationName: 'GetTlsQuery', variables: { defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], @@ -229,7 +228,6 @@ export const mockRequest = { query: 'query GetTlsQuery($sourceId: ID!, $filterQuery: String, $flowTarget: FlowTarget!, $ip: String!, $pagination: PaginationInputPaginated!, $sort: TlsSortField!, $timerange: TimerangeInput!, $defaultIndex: [String!]!, $inspect: Boolean!) {\n source(id: $sourceId) {\n id\n Tls(filterQuery: $filterQuery, flowTarget: $flowTarget, ip: $ip, pagination: $pagination, sort: $sort, timerange: $timerange, defaultIndex: $defaultIndex) {\n totalCount\n edges {\n node {\n _id\n alternativeNames\n commonNames\n ja3\n issuerNames\n notAfter\n __typename\n }\n cursor {\n value\n __typename\n }\n __typename\n }\n pageInfo {\n activePage\n fakeTotalCount\n showMorePagesIndicator\n __typename\n }\n inspect @include(if: $inspect) {\n dsl\n response\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockResponse = { diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index 9034ab4e6af83..34a50cf962412 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AuthenticatedUser } from '../../../../../plugins/security/public'; +import { RequestHandlerContext } from '../../../../../../src/core/server'; export { ConfigType as Configuration } from '../../../../../plugins/siem/server'; + import { Anomalies } from './anomalies'; import { Authentications } from './authentications'; import { Events } from './events'; @@ -54,6 +57,8 @@ export interface AppBackendLibs extends AppDomainLibs { export interface SiemContext { req: FrameworkRequest; + context: RequestHandlerContext; + user: AuthenticatedUser | null; } export interface TotalValue { diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts index 94314367be59c..e15248e5200ee 100644 --- a/x-pack/legacy/plugins/siem/server/plugin.ts +++ b/x-pack/legacy/plugins/siem/server/plugin.ts @@ -5,39 +5,71 @@ */ import { i18n } from '@kbn/i18n'; -import { CoreSetup, PluginInitializerContext, Logger } from 'src/core/server'; -import { SecurityPluginSetup } from '../../../../plugins/security/server'; -import { PluginSetupContract as FeaturesSetupContract } from '../../../../plugins/features/server'; + +import { + CoreSetup, + CoreStart, + PluginInitializerContext, + Logger, +} from '../../../../../src/core/server'; +import { SecurityPluginSetup as SecuritySetup } from '../../../../plugins/security/server'; +import { PluginSetupContract as FeaturesSetup } from '../../../../plugins/features/server'; +import { EncryptedSavedObjectsPluginSetup as EncryptedSavedObjectsSetup } from '../../../../plugins/encrypted_saved_objects/server'; +import { SpacesPluginSetup as SpacesSetup } from '../../../../plugins/spaces/server'; +import { PluginStartContract as ActionsStart } from '../../../../plugins/actions/server'; +import { LegacyServices } from './types'; import { initServer } from './init_server'; import { compose } from './lib/compose/kibana'; +import { initRoutes, LegacyInitRoutes } from './routes'; +import { isAlertExecutor } from './lib/detection_engine/signals/types'; +import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type'; import { noteSavedObjectType, pinnedEventSavedObjectType, timelineSavedObjectType, ruleStatusSavedObjectType, } from './saved_objects'; +import { ClientsService } from './services'; + +export { CoreSetup, CoreStart }; -export type SiemPluginSecurity = Pick; +export interface SetupPlugins { + encryptedSavedObjects: EncryptedSavedObjectsSetup; + features: FeaturesSetup; + security: SecuritySetup; + spaces?: SpacesSetup; +} -export interface PluginsSetup { - features: FeaturesSetupContract; - security: SiemPluginSecurity; +export interface StartPlugins { + actions: ActionsStart; } export class Plugin { readonly name = 'siem'; private readonly logger: Logger; private context: PluginInitializerContext; + private clients: ClientsService; + private legacyInitRoutes?: LegacyInitRoutes; constructor(context: PluginInitializerContext) { this.context = context; this.logger = context.logger.get('plugins', this.name); + this.clients = new ClientsService(); this.logger.debug('Shim plugin initialized'); } - public setup(core: CoreSetup, plugins: PluginsSetup) { + public setup(core: CoreSetup, plugins: SetupPlugins, __legacy: LegacyServices) { this.logger.debug('Shim plugin setup'); + + this.clients.setup(core.elasticsearch.dataClient, plugins.spaces?.spacesService); + + this.legacyInitRoutes = initRoutes( + __legacy.route, + __legacy.config, + plugins.encryptedSavedObjects?.usingEphemeralEncryptionKey ?? false + ); + plugins.features.registerFeature({ id: this.name, name: i18n.translate('xpack.siem.featureRegistry.linkSiemTitle', { @@ -98,7 +130,23 @@ export class Plugin { }, }); - const libs = compose(core, plugins, this.context.env); + if (__legacy.alerting != null) { + const type = signalRulesAlertType({ + logger: this.logger, + version: this.context.env.packageInfo.version, + }); + if (isAlertExecutor(type)) { + __legacy.alerting.setup.registerType(type); + } + } + + const libs = compose(core, plugins, this.context.env.mode.prod); initServer(libs); } + + public start(core: CoreStart, plugins: StartPlugins) { + this.clients.start(core.savedObjects, plugins.actions); + + this.legacyInitRoutes!(this.clients.createGetScoped()); + } } diff --git a/x-pack/legacy/plugins/siem/server/routes/index.ts b/x-pack/legacy/plugins/siem/server/routes/index.ts new file mode 100644 index 0000000000000..82fc4d8c11722 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/routes/index.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LegacyServices } from '../types'; +import { GetScopedClients } from '../services'; + +import { createRulesRoute } from '../lib/detection_engine/routes/rules/create_rules_route'; +import { createIndexRoute } from '../lib/detection_engine/routes/index/create_index_route'; +import { readIndexRoute } from '../lib/detection_engine/routes/index/read_index_route'; +import { readRulesRoute } from '../lib/detection_engine/routes/rules/read_rules_route'; +import { findRulesRoute } from '../lib/detection_engine/routes/rules/find_rules_route'; +import { deleteRulesRoute } from '../lib/detection_engine/routes/rules/delete_rules_route'; +import { updateRulesRoute } from '../lib/detection_engine/routes/rules/update_rules_route'; +import { patchRulesRoute } from '../lib/detection_engine/routes/rules/patch_rules_route'; +import { setSignalsStatusRoute } from '../lib/detection_engine/routes/signals/open_close_signals_route'; +import { querySignalsRoute } from '../lib/detection_engine/routes/signals/query_signals_route'; +import { deleteIndexRoute } from '../lib/detection_engine/routes/index/delete_index_route'; +import { readTagsRoute } from '../lib/detection_engine/routes/tags/read_tags_route'; +import { readPrivilegesRoute } from '../lib/detection_engine/routes/privileges/read_privileges_route'; +import { addPrepackedRulesRoute } from '../lib/detection_engine/routes/rules/add_prepackaged_rules_route'; +import { createRulesBulkRoute } from '../lib/detection_engine/routes/rules/create_rules_bulk_route'; +import { updateRulesBulkRoute } from '../lib/detection_engine/routes/rules/update_rules_bulk_route'; +import { patchRulesBulkRoute } from '../lib/detection_engine/routes/rules/patch_rules_bulk_route'; +import { deleteRulesBulkRoute } from '../lib/detection_engine/routes/rules/delete_rules_bulk_route'; +import { importRulesRoute } from '../lib/detection_engine/routes/rules/import_rules_route'; +import { exportRulesRoute } from '../lib/detection_engine/routes/rules/export_rules_route'; +import { findRulesStatusesRoute } from '../lib/detection_engine/routes/rules/find_rules_status_route'; +import { getPrepackagedRulesStatusRoute } from '../lib/detection_engine/routes/rules/get_prepackaged_rules_status_route'; + +export type LegacyInitRoutes = (getClients: GetScopedClients) => void; + +export const initRoutes = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + usingEphemeralEncryptionKey: boolean +) => (getClients: GetScopedClients): void => { + // Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules + // All REST rule creation, deletion, updating, etc...... + createRulesRoute(route, config, getClients); + readRulesRoute(route, getClients); + updateRulesRoute(route, config, getClients); + patchRulesRoute(route, getClients); + deleteRulesRoute(route, getClients); + findRulesRoute(route, getClients); + + addPrepackedRulesRoute(route, config, getClients); + getPrepackagedRulesStatusRoute(route, getClients); + createRulesBulkRoute(route, config, getClients); + updateRulesBulkRoute(route, config, getClients); + patchRulesBulkRoute(route, getClients); + deleteRulesBulkRoute(route, getClients); + + importRulesRoute(route, config, getClients); + exportRulesRoute(route, config, getClients); + + findRulesStatusesRoute(route, getClients); + + // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals + // POST /api/detection_engine/signals/status + // Example usage can be found in siem/server/lib/detection_engine/scripts/signals + setSignalsStatusRoute(route, config, getClients); + querySignalsRoute(route, config, getClients); + + // Detection Engine index routes that have the REST endpoints of /api/detection_engine/index + // All REST index creation, policy management for spaces + createIndexRoute(route, config, getClients); + readIndexRoute(route, config, getClients); + deleteIndexRoute(route, config, getClients); + + // Detection Engine tags routes that have the REST endpoints of /api/detection_engine/tags + readTagsRoute(route, getClients); + + // Privileges API to get the generic user privileges + readPrivilegesRoute(route, config, usingEphemeralEncryptionKey, getClients); +}; diff --git a/x-pack/legacy/plugins/siem/server/saved_objects.ts b/x-pack/legacy/plugins/siem/server/saved_objects.ts index 8b9a1891c8a50..58da333c7bc9a 100644 --- a/x-pack/legacy/plugins/siem/server/saved_objects.ts +++ b/x-pack/legacy/plugins/siem/server/saved_objects.ts @@ -16,6 +16,10 @@ import { ruleStatusSavedObjectMappings, ruleStatusSavedObjectType, } from './lib/detection_engine/rules/saved_object_mappings'; +import { + caseSavedObjectMappings, + caseCommentSavedObjectMappings, +} from './lib/case/saved_object_mappings'; export { noteSavedObjectType, @@ -27,5 +31,8 @@ export const savedObjectMappings = { ...timelineSavedObjectMappings, ...noteSavedObjectMappings, ...pinnedEventSavedObjectMappings, + // TODO: Remove once while Saved Object Mappings API is programmed for the NP See: https://github.com/elastic/kibana/issues/50309 + ...caseSavedObjectMappings, + ...caseCommentSavedObjectMappings, ...ruleStatusSavedObjectMappings, }; diff --git a/x-pack/legacy/plugins/siem/server/services/clients.test.ts b/x-pack/legacy/plugins/siem/server/services/clients.test.ts new file mode 100644 index 0000000000000..7f63a8f5e949c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/services/clients.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { actionsMock } from '../../../../../plugins/actions/server/mocks'; + +import { ClientsService } from './clients'; + +describe('ClientsService', () => { + describe('spacesClient', () => { + describe('#getSpaceId', () => { + it('returns the default spaceId if spaces are disabled', async () => { + const clients = new ClientsService(); + + const actions = actionsMock.createStart(); + const { elasticsearch } = coreMock.createSetup(); + const { savedObjects } = coreMock.createStart(); + const request = httpServerMock.createRawRequest(); + const spacesService = undefined; + + clients.setup(elasticsearch.dataClient, spacesService); + clients.start(savedObjects, actions); + + const { spacesClient } = await clients.createGetScoped()(request); + expect(spacesClient.getSpaceId()).toEqual('default'); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/services/clients.ts b/x-pack/legacy/plugins/siem/server/services/clients.ts new file mode 100644 index 0000000000000..ca50eda4e7a6c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/services/clients.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + IClusterClient, + IScopedClusterClient, + KibanaRequest, + LegacyRequest, + SavedObjectsClientContract, +} from '../../../../../../src/core/server'; +import { ActionsClient } from '../../../../../plugins/actions/server'; +import { AlertsClient } from '../../../../../legacy/plugins/alerting/server'; +import { SpacesServiceSetup } from '../../../../../plugins/spaces/server'; +import { CoreStart, StartPlugins } from '../plugin'; + +export interface Clients { + actionsClient?: ActionsClient; + clusterClient: IScopedClusterClient; + spacesClient: { getSpaceId: () => string }; + savedObjectsClient: SavedObjectsClientContract; +} +interface LegacyClients { + alertsClient?: AlertsClient; +} +export type GetScopedClients = (request: LegacyRequest) => Promise; + +export class ClientsService { + private actions?: StartPlugins['actions']; + private clusterClient?: IClusterClient; + private savedObjects?: CoreStart['savedObjects']; + private spacesService?: SpacesServiceSetup; + + public setup(clusterClient: IClusterClient, spacesService?: SpacesServiceSetup) { + this.clusterClient = clusterClient; + this.spacesService = spacesService; + } + + public start(savedObjects: CoreStart['savedObjects'], actions: StartPlugins['actions']) { + this.savedObjects = savedObjects; + this.actions = actions; + } + + public createGetScoped(): GetScopedClients { + if (!this.clusterClient || !this.savedObjects) { + throw new Error('Services not initialized'); + } + + return async (request: LegacyRequest) => { + const kibanaRequest = KibanaRequest.from(request); + + return { + alertsClient: request.getAlertsClient?.(), + actionsClient: await this.actions?.getActionsClientWithRequest?.(kibanaRequest), + clusterClient: this.clusterClient!.asScoped(kibanaRequest), + savedObjectsClient: this.savedObjects!.getScopedClient(kibanaRequest), + spacesClient: { + getSpaceId: () => this.spacesService?.getSpaceId?.(kibanaRequest) ?? 'default', + }, + }; + }; + } +} diff --git a/x-pack/legacy/plugins/siem/server/services/index.ts b/x-pack/legacy/plugins/siem/server/services/index.ts new file mode 100644 index 0000000000000..f4deea2c2a3fd --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/services/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ClientsService, GetScopedClients } from './clients'; diff --git a/x-pack/legacy/plugins/siem/server/types.ts b/x-pack/legacy/plugins/siem/server/types.ts index 7c07e63404eaa..e7831bb5d0451 100644 --- a/x-pack/legacy/plugins/siem/server/types.ts +++ b/x-pack/legacy/plugins/siem/server/types.ts @@ -6,28 +6,10 @@ import { Legacy } from 'kibana'; -export interface ServerFacade { +export { LegacyRequest } from '../../../../../src/core/server'; + +export interface LegacyServices { + alerting?: Legacy.Server['plugins']['alerting']; config: Legacy.Server['config']; - usingEphemeralEncryptionKey: boolean; - plugins: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - actions: any; // We have to do this at the moment because the types are not compatible - alerting?: Legacy.Server['plugins']['alerting']; - elasticsearch: Legacy.Server['plugins']['elasticsearch']; - spaces: Legacy.Server['plugins']['spaces']; - savedObjects: Legacy.Server['savedObjects']['SavedObjectsClient']; - }; route: Legacy.Server['route']; } - -export interface RequestFacade { - auth: Legacy.Request['auth']; - getAlertsClient?: Legacy.Request['getAlertsClient']; - getActionsClient?: Legacy.Request['getActionsClient']; - getSavedObjectsClient?: Legacy.Request['getSavedObjectsClient']; - headers: Legacy.Request['headers']; - method: Legacy.Request['method']; - params: Legacy.Request['params']; - payload: unknown; - query: Legacy.Request['query']; -} diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts index 13309acd03622..585f0bf7f25f5 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts @@ -10,3 +10,5 @@ export { KueryBar } from './kuerybar/kuery_bar_container'; export { FilterGroup } from './filter_group/filter_group_container'; export { MonitorStatusDetails } from './monitor/status_details_container'; export { MonitorStatusBar } from './monitor/status_bar_container'; +export { MonitorListDrawer } from './monitor/list_drawer_container'; +export { MonitorListActionsPopover } from './monitor/drawer_popover_container'; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/drawer_popover_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/drawer_popover_container.tsx new file mode 100644 index 0000000000000..be29e12f716a9 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/drawer_popover_container.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { AppState } from '../../../state'; +import { isIntegrationsPopupOpen } from '../../../state/selectors'; +import { PopoverState, toggleIntegrationsPopover } from '../../../state/actions'; +import { MonitorListActionsPopoverComponent } from '../../functional/monitor_list/monitor_list_drawer'; + +const mapStateToProps = (state: AppState) => ({ + popoverState: isIntegrationsPopupOpen(state), +}); + +const mapDispatchToProps = (dispatch: any) => ({ + togglePopoverIsVisible: (popoverState: PopoverState) => { + return dispatch(toggleIntegrationsPopover(popoverState)); + }, +}); + +export const MonitorListActionsPopover = connect( + mapStateToProps, + mapDispatchToProps +)(MonitorListActionsPopoverComponent); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/list_drawer_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/list_drawer_container.tsx new file mode 100644 index 0000000000000..8c670b485cc56 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/list_drawer_container.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, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { AppState } from '../../../state'; +import { getMonitorDetails } from '../../../state/selectors'; +import { MonitorDetailsActionPayload } from '../../../state/actions/types'; +import { fetchMonitorDetails } from '../../../state/actions/monitor'; +import { MonitorListDrawerComponent } from '../../functional/monitor_list/monitor_list_drawer/monitor_list_drawer'; +import { useUrlParams } from '../../../hooks'; +import { MonitorSummary } from '../../../../common/graphql/types'; +import { MonitorDetails } from '../../../../common/runtime_types/monitor'; + +interface ContainerProps { + summary: MonitorSummary; + monitorDetails: MonitorDetails; + loadMonitorDetails: typeof fetchMonitorDetails; +} + +const Container: React.FC = ({ summary, loadMonitorDetails, monitorDetails }) => { + const monitorId = summary?.monitor_id; + + const [getUrlParams] = useUrlParams(); + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); + + useEffect(() => { + loadMonitorDetails({ + dateStart, + dateEnd, + monitorId, + }); + }, [dateStart, dateEnd, monitorId, loadMonitorDetails]); + return ; +}; + +const mapStateToProps = (state: AppState, { summary }: any) => ({ + monitorDetails: getMonitorDetails(state, summary), +}); + +const mapDispatchToProps = (dispatch: any) => ({ + loadMonitorDetails: (actionPayload: MonitorDetailsActionPayload) => + dispatch(fetchMonitorDetails(actionPayload)), +}); + +export const MonitorListDrawer = connect(mapStateToProps, mapDispatchToProps)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx index db6337732091a..b2b555d32a3c7 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx @@ -31,7 +31,7 @@ interface OwnProps { type Props = OwnProps & StateProps & DispatchProps; -export const Container: React.FC = ({ +const Container: React.FC = ({ loadMonitorStatus, monitorId, monitorStatus, diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/index.ts index 4ec4cf4f52607..e86ba548fb5d9 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/index.ts @@ -6,7 +6,6 @@ export { DonutChart } from './charts/donut_chart'; export { EmptyState } from './empty_state'; -export { IntegrationLink } from './integration_link'; export { KueryBarComponent } from './kuery_bar/kuery_bar'; export { MonitorCharts } from './monitor_charts'; export { MonitorList } from './monitor_list'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/kuery_bar.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/kuery_bar.tsx index b1eb3f38097b2..496e8d898df3c 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/kuery_bar.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/kuery_bar.tsx @@ -15,7 +15,7 @@ import { esKuery, IIndexPattern, QuerySuggestion, - DataPublicPluginStart, + DataPublicPluginSetup, } from '../../../../../../../../src/plugins/data/public'; const Container = styled.div` @@ -33,7 +33,7 @@ function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { } interface Props { - autocomplete: DataPublicPluginStart['autocomplete']; + autocomplete: DataPublicPluginSetup['autocomplete']; loadIndexPattern: any; indexPattern: any; } diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx index de6cc1982f1a8..58250222e1330 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx @@ -28,11 +28,11 @@ import { import { MonitorListStatusColumn } from './monitor_list_status_column'; import { formatUptimeGraphQLErrorList } from '../../../lib/helper/format_error_list'; import { ExpandedRowMap } from './types'; -import { MonitorListDrawer } from './monitor_list_drawer'; import { MonitorBarSeries } from '../charts'; import { MonitorPageLink } from './monitor_page_link'; import { OverviewPageLink } from './overview_page_link'; import * as labels from './translations'; +import { MonitorListDrawer } from '../../connected'; interface MonitorListQueryResult { monitorStates?: MonitorSummaryResult; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_group.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap similarity index 66% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_group.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap index bab69a6de9708..bb578d850ff7e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_group.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap @@ -4,6 +4,15 @@ exports[`IntegrationGroup will not display APM links when APM is unavailable 1`] + + + + + + + + + + + + + + + + + + + + + `; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_link.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_link.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap index 29f2c0b63991e..cf754581b1a33 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap @@ -52,7 +52,6 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there are } > { }); it('will not display APM links when APM is unavailable', () => { - const component = shallowWithIntl( - - ); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); it('will not display infra links when infra is unavailable', () => { - const component = shallowWithIntl( - - ); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); it('will not display logging links when logging is unavailable', () => { - const component = shallowWithIntl( - - ); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/integration_link.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/integration_link.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx index c222728df3bb3..d870acefaaea6 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx @@ -12,7 +12,6 @@ import { shallowWithRouter } from '../../../../../lib'; describe('MonitorListDrawer component', () => { let summary: MonitorSummary; - let loadMonitorDetails: any; let monitorDetails: MonitorDetails; beforeEach(() => { @@ -47,16 +46,11 @@ describe('MonitorListDrawer component', () => { 'Get https://expired.badssl.com: x509: certificate has expired or is not yet valid', }, }; - loadMonitorDetails = () => null; }); it('renders nothing when no summary data is present', () => { const component = shallowWithRouter( - + ); expect(component).toEqual({}); }); @@ -64,22 +58,14 @@ describe('MonitorListDrawer component', () => { it('renders nothing when no check data is present', () => { delete summary.state.checks; const component = shallowWithRouter( - + ); expect(component).toEqual({}); }); it('renders a MonitorListDrawer when there is only one check', () => { const component = shallowWithRouter( - + ); expect(component).toMatchSnapshot(); }); @@ -110,11 +96,7 @@ describe('MonitorListDrawer component', () => { ]; summary.state.checks = checks; const component = shallowWithRouter( - + ); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/index.ts index 73fb07db60de8..2933a71c2240b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { MonitorListDrawer } from './monitor_list_drawer'; export { LocationLink } from './location_link'; +export { MonitorListActionsPopoverComponent } from './monitor_list_actions_popover'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/integration_group.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_group.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/components/functional/integration_group.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_group.tsx index da66235e37f1a..34bff58a3e2d9 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/integration_group.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_group.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; +import React, { useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -18,32 +18,29 @@ import { getLoggingContainerHref, getLoggingIpHref, getLoggingKubernetesHref, -} from '../../lib/helper'; -import { MonitorSummary } from '../../../common/graphql/types'; +} from '../../../../lib/helper'; +import { MonitorSummary } from '../../../../../common/graphql/types'; +import { UptimeSettingsContext } from '../../../../contexts'; interface IntegrationGroupProps { - basePath: string; - dateRangeStart: string; - dateRangeEnd: string; - isApmAvailable: boolean; - isInfraAvailable: boolean; - isLogsAvailable: boolean; summary: MonitorSummary; } -export const IntegrationGroup = ({ - basePath, - dateRangeStart, - dateRangeEnd, - isApmAvailable, - isInfraAvailable, - isLogsAvailable, - summary, -}: IntegrationGroupProps) => { +export const IntegrationGroup = ({ summary }: IntegrationGroupProps) => { + const { + basePath, + dateRangeStart, + dateRangeEnd, + isApmAvailable, + isInfraAvailable, + isLogsAvailable, + } = useContext(UptimeSettingsContext); + const domain = get(summary, 'state.url.domain', ''); const podUid = get(summary, 'state.checks[0].kubernetes.pod.uid', undefined); const containerId = get(summary, 'state.checks[0].container.id', undefined); const ip = get(summary, 'state.checks[0].monitor.ip', undefined); + return isApmAvailable || isInfraAvailable || isLogsAvailable ? ( {isApmAvailable ? ( diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/integration_link.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/integration_link.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_link.tsx index a545cd7c42927..4b4c2003931a3 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/integration_link.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_link.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; interface IntegrationLinkProps { ariaLabel: string; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_actions_popover.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_actions_popover.tsx similarity index 58% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_actions_popover.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_actions_popover.tsx index af06761f50c83..6b946baa8d403 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_actions_popover.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_actions_popover.tsx @@ -4,17 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPopover, EuiButton } from '@elastic/eui'; -import React, { useContext } from 'react'; -import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { connect } from 'react-redux'; -import { MonitorSummary } from '../../../../common/graphql/types'; -import { IntegrationGroup } from '../integration_group'; -import { UptimeSettingsContext } from '../../../contexts'; -import { isIntegrationsPopupOpen } from '../../../state/selectors'; -import { AppState } from '../../../state'; -import { toggleIntegrationsPopover, PopoverState } from '../../../state/actions'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { EuiPopover, EuiButton } from '@elastic/eui'; +import { IntegrationGroup } from './integration_group'; +import { MonitorSummary } from '../../../../../common/graphql/types'; +import { toggleIntegrationsPopover, PopoverState } from '../../../../state/actions'; interface MonitorListActionsPopoverProps { summary: MonitorSummary; @@ -22,20 +18,12 @@ interface MonitorListActionsPopoverProps { togglePopoverIsVisible: typeof toggleIntegrationsPopover; } -const MonitorListActionsPopoverComponent = ({ +export const MonitorListActionsPopoverComponent = ({ summary, popoverState, togglePopoverIsVisible, }: MonitorListActionsPopoverProps) => { const popoverId = `${summary.monitor_id}_popover`; - const { - basePath, - dateRangeStart, - dateRangeEnd, - isApmAvailable, - isInfraAvailable, - isLogsAvailable, - } = useContext(UptimeSettingsContext); const monitorUrl: string | undefined = get(summary, 'state.url.full', undefined); const isPopoverOpen: boolean = @@ -64,30 +52,7 @@ const MonitorListActionsPopoverComponent = ({ id={popoverId} isOpen={isPopoverOpen} > - + ); }; - -const mapStateToProps = (state: AppState) => ({ - popoverState: isIntegrationsPopupOpen(state), -}); - -const mapDispatchToProps = (dispatch: any) => ({ - togglePopoverIsVisible: (popoverState: PopoverState) => { - return dispatch(toggleIntegrationsPopover(popoverState)); - }, -}); - -export const MonitorListActionsPopover = connect( - mapStateToProps, - mapDispatchToProps -)(MonitorListActionsPopoverComponent); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx index 35b649fa35795..8383596ccc346 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx @@ -4,20 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; -import { EuiLink, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; +import React from 'react'; import styled from 'styled-components'; -import { connect } from 'react-redux'; +import { EuiLink, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; import { MonitorSummary } from '../../../../../common/graphql/types'; -import { AppState } from '../../../../state'; -import { fetchMonitorDetails } from '../../../../state/actions/monitor'; import { MostRecentError } from './most_recent_error'; -import { getMonitorDetails } from '../../../../state/selectors'; import { MonitorStatusList } from './monitor_status_list'; import { MonitorDetails } from '../../../../../common/runtime_types'; -import { useUrlParams } from '../../../../hooks'; -import { MonitorDetailsActionPayload } from '../../../../state/actions/types'; -import { MonitorListActionsPopover } from '../monitor_list_actions_popover'; +import { MonitorListActionsPopover } from '../../../connected'; const ContainerDiv = styled.div` padding: 10px; @@ -34,34 +28,13 @@ interface MonitorListDrawerProps { * Monitor details to be fetched from rest api using monitorId */ monitorDetails: MonitorDetails; - - /** - * Redux action to trigger , loading monitor details - */ - loadMonitorDetails: typeof fetchMonitorDetails; } /** * The elements shown when the user expands the monitor list rows. */ -export function MonitorListDrawerComponent({ - summary, - loadMonitorDetails, - monitorDetails, -}: MonitorListDrawerProps) { - const monitorId = summary?.monitor_id; - const [getUrlParams] = useUrlParams(); - const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); - - useEffect(() => { - loadMonitorDetails({ - dateStart, - dateEnd, - monitorId, - }); - }, [dateStart, dateEnd, monitorId, loadMonitorDetails]); - +export function MonitorListDrawerComponent({ summary, monitorDetails }: MonitorListDrawerProps) { const monitorUrl = summary?.state?.url?.full || ''; return summary && summary.state.checks ? ( @@ -91,17 +64,3 @@ export function MonitorListDrawerComponent({ ) : null; } - -const mapStateToProps = (state: AppState, { summary }: any) => ({ - monitorDetails: getMonitorDetails(state, summary), -}); - -const mapDispatchToProps = (dispatch: any) => ({ - loadMonitorDetails: (actionPayload: MonitorDetailsActionPayload) => - dispatch(fetchMonitorDetails(actionPayload)), -}); - -export const MonitorListDrawer = connect( - mapStateToProps, - mapDispatchToProps -)(MonitorListDrawerComponent); diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap index fff947bd96024..71b3fb5c7146a 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap @@ -54,6 +54,7 @@ exports[`MonitorPage shallow renders expected elements for valid props 1`] = ` { getQuerySuggestions: jest.fn(), hasQuerySuggestions: () => true, getValueSuggestions: jest.fn(), + addQuerySuggestionProvider: jest.fn(), }; it('shallow renders expected elements for valid props', () => { diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index ae7457e835c94..5360d66f87e99 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -16,13 +16,13 @@ import { import { useUrlParams, useUptimeTelemetry, UptimePage } from '../hooks'; import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { useTrackPageview } from '../../../infra/public'; -import { DataPublicPluginStart, IIndexPattern } from '../../../../../../src/plugins/data/public'; +import { DataPublicPluginSetup, IIndexPattern } from '../../../../../../src/plugins/data/public'; import { UptimeThemeContext } from '../contexts'; import { FilterGroup, KueryBar } from '../components/connected'; import { useUpdateKueryString } from '../hooks'; interface OverviewPageProps { - autocomplete: DataPublicPluginStart['autocomplete']; + autocomplete: DataPublicPluginSetup['autocomplete']; indexPattern: IIndexPattern; setEsKueryFilters: (esFilters: string) => void; } diff --git a/x-pack/legacy/plugins/uptime/public/routes.tsx b/x-pack/legacy/plugins/uptime/public/routes.tsx index 0f726d89e0d28..83be45083b645 100644 --- a/x-pack/legacy/plugins/uptime/public/routes.tsx +++ b/x-pack/legacy/plugins/uptime/public/routes.tsx @@ -6,13 +6,13 @@ import React, { FC } from 'react'; import { Route, Switch } from 'react-router-dom'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; import { OverviewPage } from './components/connected/pages/overview_container'; import { MONITOR_ROUTE, OVERVIEW_ROUTE } from '../common/constants'; import { MonitorPage, NotFoundPage } from './pages'; interface RouterProps { - autocomplete: DataPublicPluginStart['autocomplete']; + autocomplete: DataPublicPluginSetup['autocomplete']; } export const PageRouter: FC = ({ autocomplete }) => ( diff --git a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx index dbde9f8b6a8c0..db34566d6c148 100644 --- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx @@ -100,7 +100,6 @@ const Application = (props: UptimeAppProps) => { - // @ts-ignore we need to update the type of this prop diff --git a/x-pack/plugins/case/server/index.ts b/x-pack/plugins/case/server/index.ts index 3963debea9795..990aef19b74f7 100644 --- a/x-pack/plugins/case/server/index.ts +++ b/x-pack/plugins/case/server/index.ts @@ -7,7 +7,7 @@ import { PluginInitializerContext } from '../../../../src/core/server'; import { ConfigSchema } from './config'; import { CasePlugin } from './plugin'; -export { NewCaseFormatted, NewCommentFormatted } from './routes/api/types'; +export { CaseAttributes, CommentAttributes } from './routes/api/types'; export const config = { schema: ConfigSchema }; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts index 360c6de67b2a8..eb9afb27a749e 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts @@ -21,6 +21,8 @@ export const createMockSavedObjectsRepository = (savedObject: any[] = []) => { throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); } return { + page: 1, + per_page: 5, total: savedObject.length, saved_objects: savedObject, }; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts index 84889c3ac49be..ac9eddd6dd2cb 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts @@ -12,7 +12,7 @@ import { RouteDeps } from '../index'; export const createRoute = async ( api: (deps: RouteDeps) => void, - method: 'get' | 'post' | 'delete', + method: 'get' | 'post' | 'delete' | 'patch', badAuth = false ) => { const httpService = httpServiceMock.createSetupContract(); diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index d59f0977e6993..c7f6b6fad7d1a 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -9,17 +9,16 @@ export const mockCases = [ type: 'case-workflow', id: 'mock-id-1', attributes: { - created_at: 1574718888885, + created_at: '2019-11-25T21:54:48.952Z', created_by: { - full_name: null, + full_name: 'elastic', username: 'elastic', }, description: 'This is a brand new case of a bad meanie defacing data', title: 'Super Bad Security Issue', state: 'open', tags: ['defacement'], - case_type: 'security', - assignees: [], + updated_at: '2019-11-25T21:54:48.952Z', }, references: [], updated_at: '2019-11-25T21:54:48.952Z', @@ -29,17 +28,16 @@ export const mockCases = [ type: 'case-workflow', id: 'mock-id-2', attributes: { - created_at: 1574721120834, + created_at: '2019-11-25T22:32:00.900Z', created_by: { - full_name: null, + full_name: 'elastic', username: 'elastic', }, description: 'Oh no, a bad meanie destroying data!', title: 'Damaging Data Destruction Detected', state: 'open', tags: ['Data Destruction'], - case_type: 'security', - assignees: [], + updated_at: '2019-11-25T22:32:00.900Z', }, references: [], updated_at: '2019-11-25T22:32:00.900Z', @@ -49,17 +47,16 @@ export const mockCases = [ type: 'case-workflow', id: 'mock-id-3', attributes: { - created_at: 1574721137881, + created_at: '2019-11-25T22:32:17.947Z', created_by: { - full_name: null, + full_name: 'elastic', username: 'elastic', }, description: 'Oh no, a bad meanie going LOLBins all over the place!', title: 'Another bad one', state: 'open', tags: ['LOLBins'], - case_type: 'security', - assignees: [], + updated_at: '2019-11-25T22:32:17.947Z', }, references: [], updated_at: '2019-11-25T22:32:17.947Z', @@ -82,11 +79,12 @@ export const mockCaseComments = [ id: 'mock-comment-1', attributes: { comment: 'Wow, good luck catching that bad meanie!', - created_at: 1574718900112, + created_at: '2019-11-25T21:55:00.177Z', created_by: { - full_name: null, + full_name: 'elastic', username: 'elastic', }, + updated_at: '2019-11-25T21:55:00.177Z', }, references: [ { @@ -103,11 +101,12 @@ export const mockCaseComments = [ id: 'mock-comment-2', attributes: { comment: 'Well I decided to update my comment. So what? Deal with it.', - created_at: 1574718902724, + created_at: '2019-11-25T21:55:14.633Z', created_by: { - full_name: null, + full_name: 'elastic', username: 'elastic', }, + updated_at: '2019-11-25T21:55:14.633Z', }, references: [ { @@ -124,11 +123,12 @@ export const mockCaseComments = [ id: 'mock-comment-3', attributes: { comment: 'Wow, good luck catching that bad meanie!', - created_at: 1574721150542, + created_at: '2019-11-25T22:32:30.608Z', created_by: { - full_name: null, + full_name: 'elastic', username: 'elastic', }, + updated_at: '2019-11-25T22:32:30.608Z', }, references: [ { diff --git a/x-pack/plugins/case/server/routes/api/__tests__/get_all_cases.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/get_all_cases.test.ts index 2f8a229c08f29..96c411a746d49 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/get_all_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/get_all_cases.test.ts @@ -19,7 +19,7 @@ describe('GET all cases', () => { beforeAll(async () => { routeHandler = await createRoute(initGetAllCasesApi, 'get'); }); - it(`returns the case without case comments when includeComments is false`, async () => { + it(`gets all the cases`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases', method: 'get', @@ -29,6 +29,6 @@ describe('GET all cases', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.saved_objects).toHaveLength(3); + expect(response.payload.cases).toHaveLength(3); }); }); diff --git a/x-pack/plugins/case/server/routes/api/__tests__/get_case.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/get_case.test.ts index 3c5f8e52d1946..60becf1228a0c 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/get_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/get_case.test.ts @@ -12,15 +12,17 @@ import { mockCasesErrorTriggerData, } from '../__fixtures__'; import { initGetCaseApi } from '../get_case'; -import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { kibanaResponseFactory, RequestHandler, SavedObject } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; +import { flattenCaseSavedObject } from '../utils'; +import { CaseAttributes } from '../types'; describe('GET case', () => { let routeHandler: RequestHandler; beforeAll(async () => { routeHandler = await createRoute(initGetCaseApi, 'get'); }); - it(`returns the case without case comments when includeComments is false`, async () => { + it(`returns the case with empty case comments when includeComments is false`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases/{id}', params: { @@ -37,8 +39,13 @@ describe('GET case', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload).toEqual(mockCases.find(s => s.id === 'mock-id-1')); - expect(response.payload.comments).toBeUndefined(); + expect(response.payload).toEqual( + flattenCaseSavedObject( + (mockCases.find(s => s.id === 'mock-id-1') as unknown) as SavedObject, + [] + ) + ); + expect(response.payload.comments).toEqual([]); }); it(`returns an error when thrown from getCase`, async () => { const request = httpServerMock.createKibanaRequest({ @@ -76,7 +83,7 @@ describe('GET case', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.comments.saved_objects).toHaveLength(3); + expect(response.payload.comments).toHaveLength(3); }); it(`returns an error when thrown from getAllCaseComments`, async () => { const request = httpServerMock.createKibanaRequest({ diff --git a/x-pack/plugins/case/server/routes/api/__tests__/get_comment.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/get_comment.test.ts index 9b6a1e435838b..3add93acc641f 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/get_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/get_comment.test.ts @@ -11,8 +11,10 @@ import { mockCaseComments, } from '../__fixtures__'; import { initGetCommentApi } from '../get_comment'; -import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { kibanaResponseFactory, RequestHandler, SavedObject } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; +import { flattenCommentSavedObject } from '../utils'; +import { CommentAttributes } from '../types'; describe('GET comment', () => { let routeHandler: RequestHandler; @@ -32,7 +34,11 @@ describe('GET comment', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload).toEqual(mockCaseComments.find(s => s.id === 'mock-comment-1')); + expect(response.payload).toEqual( + flattenCommentSavedObject( + mockCaseComments.find(s => s.id === 'mock-comment-1') as SavedObject + ) + ); }); it(`returns an error when getComment throws`, async () => { const request = httpServerMock.createKibanaRequest({ diff --git a/x-pack/plugins/case/server/routes/api/__tests__/post_case.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/post_case.test.ts index bb688dde4c58f..32c7c5a015af0 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/post_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/post_case.test.ts @@ -28,7 +28,6 @@ describe('POST cases', () => { title: 'Super Bad Security Issue', state: 'open', tags: ['defacement'], - case_type: 'security', }, }); @@ -36,8 +35,8 @@ describe('POST cases', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.id).toEqual('mock-it'); - expect(response.payload.attributes.created_by.username).toEqual('awesome'); + expect(response.payload.case_id).toEqual('mock-it'); + expect(response.payload.created_by.username).toEqual('awesome'); }); it(`Returns an error if postNewCase throws`, async () => { const request = httpServerMock.createKibanaRequest({ @@ -48,7 +47,6 @@ describe('POST cases', () => { title: 'Super Bad Security Issue', state: 'open', tags: ['error'], - case_type: 'security', }, }); @@ -69,7 +67,6 @@ describe('POST cases', () => { title: 'Super Bad Security Issue', state: 'open', tags: ['defacement'], - case_type: 'security', }, }); diff --git a/x-pack/plugins/case/server/routes/api/__tests__/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/post_comment.test.ts index 0c059b7f15ea4..653140af2a7cf 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/post_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/post_comment.test.ts @@ -35,8 +35,7 @@ describe('POST comment', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.id).toEqual('mock-comment'); - expect(response.payload.references[0].id).toEqual('mock-id-1'); + expect(response.payload.comment_id).toEqual('mock-comment'); }); it(`Returns an error if the case does not exist`, async () => { const request = httpServerMock.createKibanaRequest({ diff --git a/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts index 7ed478d2e7c01..23283d7f8a5be 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts @@ -17,12 +17,12 @@ import { httpServerMock } from 'src/core/server/mocks'; describe('UPDATE case', () => { let routeHandler: RequestHandler; beforeAll(async () => { - routeHandler = await createRoute(initUpdateCaseApi, 'post'); + routeHandler = await createRoute(initUpdateCaseApi, 'patch'); }); it(`Updates a case`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases/{id}', - method: 'post', + method: 'patch', params: { id: 'mock-id-1', }, @@ -35,13 +35,13 @@ describe('UPDATE case', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.id).toEqual('mock-id-1'); - expect(response.payload.attributes.state).toEqual('closed'); + expect(typeof response.payload.updated_at).toBe('string'); + expect(response.payload.state).toEqual('closed'); }); it(`Returns an error if updateCase throws`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases/{id}', - method: 'post', + method: 'patch', params: { id: 'mock-id-does-not-exist', }, diff --git a/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts index 8aa84b45b7dbb..5bfd121691ab4 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts @@ -17,12 +17,12 @@ import { httpServerMock } from 'src/core/server/mocks'; describe('UPDATE comment', () => { let routeHandler: RequestHandler; beforeAll(async () => { - routeHandler = await createRoute(initUpdateCommentApi, 'post'); + routeHandler = await createRoute(initUpdateCommentApi, 'patch'); }); it(`Updates a comment`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases/comment/{id}', - method: 'post', + method: 'patch', params: { id: 'mock-comment-1', }, @@ -35,13 +35,12 @@ describe('UPDATE comment', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.id).toEqual('mock-comment-1'); - expect(response.payload.attributes.comment).toEqual('Update my comment'); + expect(response.payload.comment).toEqual('Update my comment'); }); it(`Returns an error if updateComment throws`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases/comment/{id}', - method: 'post', + method: 'patch', params: { id: 'mock-comment-does-not-exist', }, diff --git a/x-pack/plugins/case/server/routes/api/get_all_case_comments.ts b/x-pack/plugins/case/server/routes/api/get_all_case_comments.ts index cc4956ead1bd7..b74227fa8d983 100644 --- a/x-pack/plugins/case/server/routes/api/get_all_case_comments.ts +++ b/x-pack/plugins/case/server/routes/api/get_all_case_comments.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '.'; -import { wrapError } from './utils'; +import { formatAllComments, wrapError } from './utils'; export function initGetAllCaseCommentsApi({ caseService, router }: RouteDeps) { router.get( @@ -24,7 +24,7 @@ export function initGetAllCaseCommentsApi({ caseService, router }: RouteDeps) { client: context.core.savedObjects.client, caseId: request.params.id, }); - return response.ok({ body: theComments }); + return response.ok({ body: formatAllComments(theComments) }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/get_all_cases.ts b/x-pack/plugins/case/server/routes/api/get_all_cases.ts index 749a183dfe980..09075a32ac377 100644 --- a/x-pack/plugins/case/server/routes/api/get_all_cases.ts +++ b/x-pack/plugins/case/server/routes/api/get_all_cases.ts @@ -4,21 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ +import { schema } from '@kbn/config-schema'; import { RouteDeps } from '.'; -import { wrapError } from './utils'; +import { formatAllCases, wrapError } from './utils'; +import { SavedObjectsFindOptionsSchema } from './schema'; +import { AllCases } from './types'; export function initGetAllCasesApi({ caseService, router }: RouteDeps) { router.get( { path: '/api/cases', - validate: false, + validate: { + query: schema.nullable(SavedObjectsFindOptionsSchema), + }, }, async (context, request, response) => { try { - const cases = await caseService.getAllCases({ - client: context.core.savedObjects.client, + const args = request.query + ? { + client: context.core.savedObjects.client, + options: request.query, + } + : { + client: context.core.savedObjects.client, + }; + const cases = await caseService.getAllCases(args); + const body: AllCases = formatAllCases(cases); + return response.ok({ + body, }); - return response.ok({ body: cases }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/get_case.ts b/x-pack/plugins/case/server/routes/api/get_case.ts index 6aad22a1ebf1b..2481197000beb 100644 --- a/x-pack/plugins/case/server/routes/api/get_case.ts +++ b/x-pack/plugins/case/server/routes/api/get_case.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '.'; -import { wrapError } from './utils'; +import { flattenCaseSavedObject, wrapError } from './utils'; export function initGetCaseApi({ caseService, router }: RouteDeps) { router.get( @@ -33,14 +33,16 @@ export function initGetCaseApi({ caseService, router }: RouteDeps) { return response.customError(wrapError(error)); } if (!includeComments) { - return response.ok({ body: theCase }); + return response.ok({ body: flattenCaseSavedObject(theCase, []) }); } try { const theComments = await caseService.getAllCaseComments({ client: context.core.savedObjects.client, caseId: request.params.id, }); - return response.ok({ body: { ...theCase, comments: theComments } }); + return response.ok({ + body: { ...flattenCaseSavedObject(theCase, theComments.saved_objects) }, + }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/get_comment.ts b/x-pack/plugins/case/server/routes/api/get_comment.ts index 6fd507d89738d..d892b4cfebc3b 100644 --- a/x-pack/plugins/case/server/routes/api/get_comment.ts +++ b/x-pack/plugins/case/server/routes/api/get_comment.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '.'; -import { wrapError } from './utils'; +import { flattenCommentSavedObject, wrapError } from './utils'; export function initGetCommentApi({ caseService, router }: RouteDeps) { router.get( @@ -24,7 +24,7 @@ export function initGetCommentApi({ caseService, router }: RouteDeps) { client: context.core.savedObjects.client, commentId: request.params.id, }); - return response.ok({ body: theComment }); + return response.ok({ body: flattenCommentSavedObject(theComment) }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/get_tags.ts b/x-pack/plugins/case/server/routes/api/get_tags.ts new file mode 100644 index 0000000000000..1d714db4c0c28 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/get_tags.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDeps } from './index'; +import { wrapError } from './utils'; + +export function initGetTagsApi({ caseService, router }: RouteDeps) { + router.get( + { + path: '/api/cases/tags', + validate: {}, + }, + async (context, request, response) => { + let theCase; + try { + theCase = await caseService.getTags({ + client: context.core.savedObjects.client, + }); + return response.ok({ body: theCase }); + } catch (error) { + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/case/server/routes/api/index.ts b/x-pack/plugins/case/server/routes/api/index.ts index 11ef91d539e87..32dfd6a78d1c2 100644 --- a/x-pack/plugins/case/server/routes/api/index.ts +++ b/x-pack/plugins/case/server/routes/api/index.ts @@ -5,17 +5,18 @@ */ import { IRouter } from 'src/core/server'; -import { initDeleteCommentApi } from './delete_comment'; +import { CaseServiceSetup } from '../../services'; import { initDeleteCaseApi } from './delete_case'; +import { initDeleteCommentApi } from './delete_comment'; import { initGetAllCaseCommentsApi } from './get_all_case_comments'; import { initGetAllCasesApi } from './get_all_cases'; import { initGetCaseApi } from './get_case'; import { initGetCommentApi } from './get_comment'; +import { initGetTagsApi } from './get_tags'; import { initPostCaseApi } from './post_case'; import { initPostCommentApi } from './post_comment'; import { initUpdateCaseApi } from './update_case'; import { initUpdateCommentApi } from './update_comment'; -import { CaseServiceSetup } from '../../services'; export interface RouteDeps { caseService: CaseServiceSetup; @@ -23,12 +24,13 @@ export interface RouteDeps { } export function initCaseApi(deps: RouteDeps) { + initDeleteCaseApi(deps); + initDeleteCommentApi(deps); initGetAllCaseCommentsApi(deps); initGetAllCasesApi(deps); initGetCaseApi(deps); initGetCommentApi(deps); - initDeleteCaseApi(deps); - initDeleteCommentApi(deps); + initGetTagsApi(deps); initPostCaseApi(deps); initPostCommentApi(deps); initUpdateCaseApi(deps); diff --git a/x-pack/plugins/case/server/routes/api/post_case.ts b/x-pack/plugins/case/server/routes/api/post_case.ts index e5aa0a3548b48..948bf02d5b3c1 100644 --- a/x-pack/plugins/case/server/routes/api/post_case.ts +++ b/x-pack/plugins/case/server/routes/api/post_case.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { formatNewCase, wrapError } from './utils'; +import { flattenCaseSavedObject, formatNewCase, wrapError } from './utils'; import { NewCaseSchema } from './schema'; import { RouteDeps } from '.'; @@ -31,7 +31,7 @@ export function initPostCaseApi({ caseService, router }: RouteDeps) { ...createdBy, }), }); - return response.ok({ body: newCase }); + return response.ok({ body: flattenCaseSavedObject(newCase, []) }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/post_comment.ts b/x-pack/plugins/case/server/routes/api/post_comment.ts index 3f4592f5bb11f..f3f21becddfad 100644 --- a/x-pack/plugins/case/server/routes/api/post_comment.ts +++ b/x-pack/plugins/case/server/routes/api/post_comment.ts @@ -5,7 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { formatNewComment, wrapError } from './utils'; +import { flattenCommentSavedObject, formatNewComment, wrapError } from './utils'; import { NewCommentSchema } from './schema'; import { RouteDeps } from '.'; import { CASE_SAVED_OBJECT } from '../../constants'; @@ -53,7 +53,7 @@ export function initPostCommentApi({ caseService, router }: RouteDeps) { ], }); - return response.ok({ body: newComment }); + return response.ok({ body: flattenCommentSavedObject(newComment) }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/schema.ts b/x-pack/plugins/case/server/routes/api/schema.ts index 4a4a0c3a11e36..962dc474254f0 100644 --- a/x-pack/plugins/case/server/routes/api/schema.ts +++ b/x-pack/plugins/case/server/routes/api/schema.ts @@ -7,8 +7,8 @@ import { schema } from '@kbn/config-schema'; export const UserSchema = schema.object({ - username: schema.string(), full_name: schema.maybe(schema.string()), + username: schema.string(), }); export const NewCommentSchema = schema.object({ @@ -17,28 +17,38 @@ export const NewCommentSchema = schema.object({ export const CommentSchema = schema.object({ comment: schema.string(), - created_at: schema.number(), + created_at: schema.string(), created_by: UserSchema, + updated_at: schema.string(), }); export const UpdatedCommentSchema = schema.object({ comment: schema.string(), + updated_at: schema.string(), }); export const NewCaseSchema = schema.object({ - assignees: schema.arrayOf(UserSchema, { defaultValue: [] }), description: schema.string(), - title: schema.string(), state: schema.oneOf([schema.literal('open'), schema.literal('closed')], { defaultValue: 'open' }), tags: schema.arrayOf(schema.string(), { defaultValue: [] }), - case_type: schema.string(), + title: schema.string(), }); export const UpdatedCaseSchema = schema.object({ - assignees: schema.maybe(schema.arrayOf(UserSchema)), description: schema.maybe(schema.string()), - title: schema.maybe(schema.string()), state: schema.maybe(schema.oneOf([schema.literal('open'), schema.literal('closed')])), tags: schema.maybe(schema.arrayOf(schema.string())), - case_type: schema.maybe(schema.string()), + title: schema.maybe(schema.string()), +}); + +export const SavedObjectsFindOptionsSchema = schema.object({ + defaultSearchOperator: schema.maybe(schema.oneOf([schema.literal('AND'), schema.literal('OR')])), + fields: schema.maybe(schema.arrayOf(schema.string())), + filter: schema.maybe(schema.string()), + page: schema.maybe(schema.number()), + perPage: schema.maybe(schema.number()), + search: schema.maybe(schema.string()), + searchFields: schema.maybe(schema.arrayOf(schema.string())), + sortField: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])), }); diff --git a/x-pack/plugins/case/server/routes/api/types.ts b/x-pack/plugins/case/server/routes/api/types.ts index d943e4e5fd7dd..2d1a88bcf1429 100644 --- a/x-pack/plugins/case/server/routes/api/types.ts +++ b/x-pack/plugins/case/server/routes/api/types.ts @@ -9,28 +9,63 @@ import { CommentSchema, NewCaseSchema, NewCommentSchema, + SavedObjectsFindOptionsSchema, UpdatedCaseSchema, UpdatedCommentSchema, UserSchema, } from './schema'; +import { SavedObjectAttributes } from '../../../../../../src/core/types'; export type NewCaseType = TypeOf; -export type NewCommentFormatted = TypeOf; +export type CommentAttributes = TypeOf & SavedObjectAttributes; export type NewCommentType = TypeOf; +export type SavedObjectsFindOptionsType = TypeOf; export type UpdatedCaseTyped = TypeOf; export type UpdatedCommentType = TypeOf; export type UserType = TypeOf; -export interface NewCaseFormatted extends NewCaseType { - created_at: number; +export interface CaseAttributes extends NewCaseType, SavedObjectAttributes { + created_at: string; created_by: UserType; + updated_at: string; +} + +export type FlattenedCaseSavedObject = CaseAttributes & { + case_id: string; + comments: FlattenedCommentSavedObject[]; +}; + +export type FlattenedCasesSavedObject = Array< + CaseAttributes & { + case_id: string; + // TO DO it is partial because we need to add it the commentCount + commentCount?: number; + } +>; + +export interface AllCases { + cases: FlattenedCasesSavedObject; + page: number; + per_page: number; + total: number; +} + +export type FlattenedCommentSavedObject = CommentAttributes & { + comment_id: string; + // TO DO We might want to add the case_id where this comment is related too +}; + +export interface AllComments { + comments: FlattenedCommentSavedObject[]; + page: number; + per_page: number; + total: number; } export interface UpdatedCaseType { - assignees?: UpdatedCaseTyped['assignees']; description?: UpdatedCaseTyped['description']; - title?: UpdatedCaseTyped['title']; state?: UpdatedCaseTyped['state']; tags?: UpdatedCaseTyped['tags']; - case_type?: UpdatedCaseTyped['case_type']; + title?: UpdatedCaseTyped['title']; + updated_at: string; } diff --git a/x-pack/plugins/case/server/routes/api/update_case.ts b/x-pack/plugins/case/server/routes/api/update_case.ts index 52c8cab0022dd..2a814c7259e4a 100644 --- a/x-pack/plugins/case/server/routes/api/update_case.ts +++ b/x-pack/plugins/case/server/routes/api/update_case.ts @@ -10,7 +10,7 @@ import { RouteDeps } from '.'; import { UpdatedCaseSchema } from './schema'; export function initUpdateCaseApi({ caseService, router }: RouteDeps) { - router.post( + router.patch( { path: '/api/cases/{id}', validate: { @@ -25,9 +25,12 @@ export function initUpdateCaseApi({ caseService, router }: RouteDeps) { const updatedCase = await caseService.updateCase({ client: context.core.savedObjects.client, caseId: request.params.id, - updatedAttributes: request.body, + updatedAttributes: { + ...request.body, + updated_at: new Date().toISOString(), + }, }); - return response.ok({ body: updatedCase }); + return response.ok({ body: updatedCase.attributes }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/update_comment.ts b/x-pack/plugins/case/server/routes/api/update_comment.ts index e1ee6029e8e4f..815f44a14e2e7 100644 --- a/x-pack/plugins/case/server/routes/api/update_comment.ts +++ b/x-pack/plugins/case/server/routes/api/update_comment.ts @@ -10,7 +10,7 @@ import { NewCommentSchema } from './schema'; import { RouteDeps } from '.'; export function initUpdateCommentApi({ caseService, router }: RouteDeps) { - router.post( + router.patch( { path: '/api/cases/comment/{id}', validate: { @@ -25,9 +25,12 @@ export function initUpdateCommentApi({ caseService, router }: RouteDeps) { const updatedComment = await caseService.updateComment({ client: context.core.savedObjects.client, commentId: request.params.id, - updatedAttributes: request.body, + updatedAttributes: { + ...request.body, + updated_at: new Date().toISOString(), + }, }); - return response.ok({ body: updatedComment }); + return response.ok({ body: updatedComment.attributes }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index c6e33dbb8433b..51944b04836ab 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -5,21 +5,31 @@ */ import { boomify, isBoom } from 'boom'; -import { CustomHttpResponseOptions, ResponseError } from 'kibana/server'; import { + CustomHttpResponseOptions, + ResponseError, + SavedObject, + SavedObjectsFindResponse, +} from 'kibana/server'; +import { + AllComments, + CaseAttributes, + CommentAttributes, + FlattenedCaseSavedObject, + FlattenedCommentSavedObject, + AllCases, NewCaseType, - NewCaseFormatted, NewCommentType, - NewCommentFormatted, UserType, } from './types'; export const formatNewCase = ( newCase: NewCaseType, { full_name, username }: { full_name?: string; username: string } -): NewCaseFormatted => ({ - created_at: new Date().valueOf(), +): CaseAttributes => ({ + created_at: new Date().toISOString(), created_by: { full_name, username }, + updated_at: new Date().toISOString(), ...newCase, }); @@ -32,10 +42,11 @@ export const formatNewComment = ({ newComment, full_name, username, -}: NewCommentArgs): NewCommentFormatted => ({ +}: NewCommentArgs): CommentAttributes => ({ ...newComment, - created_at: new Date().valueOf(), + created_at: new Date().toISOString(), created_by: { full_name, username }, + updated_at: new Date().toISOString(), }); export function wrapError(error: any): CustomHttpResponseOptions { @@ -46,3 +57,55 @@ export function wrapError(error: any): CustomHttpResponseOptions statusCode: boom.output.statusCode, }; } + +export const formatAllCases = (cases: SavedObjectsFindResponse): AllCases => ({ + page: cases.page, + per_page: cases.per_page, + total: cases.total, + cases: flattenCaseSavedObjects(cases.saved_objects), +}); + +export const flattenCaseSavedObjects = ( + savedObjects: SavedObjectsFindResponse['saved_objects'] +): FlattenedCaseSavedObject[] => + savedObjects.reduce( + (acc: FlattenedCaseSavedObject[], savedObject: SavedObject) => { + return [...acc, flattenCaseSavedObject(savedObject, [])]; + }, + [] + ); + +export const flattenCaseSavedObject = ( + savedObject: SavedObject, + comments: Array> +): FlattenedCaseSavedObject => ({ + case_id: savedObject.id, + comments: flattenCommentSavedObjects(comments), + ...savedObject.attributes, +}); + +export const formatAllComments = ( + comments: SavedObjectsFindResponse +): AllComments => ({ + page: comments.page, + per_page: comments.per_page, + total: comments.total, + comments: flattenCommentSavedObjects(comments.saved_objects), +}); + +export const flattenCommentSavedObjects = ( + savedObjects: SavedObjectsFindResponse['saved_objects'] +): FlattenedCommentSavedObject[] => + savedObjects.reduce( + (acc: FlattenedCommentSavedObject[], savedObject: SavedObject) => { + return [...acc, flattenCommentSavedObject(savedObject)]; + }, + [] + ); + +export const flattenCommentSavedObject = ( + savedObject: SavedObject +): FlattenedCommentSavedObject => ({ + comment_id: savedObject.id, + ...savedObject.attributes, +}); diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index 531d5fa5b87e5..d6d4bd606676c 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -16,12 +16,14 @@ import { } from 'kibana/server'; import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../constants'; import { - NewCaseFormatted, - NewCommentFormatted, + CaseAttributes, + CommentAttributes, + SavedObjectsFindOptionsType, UpdatedCaseType, UpdatedCommentType, } from '../routes/api/types'; import { AuthenticatedUser, SecurityPluginSetup } from '../../../security/server'; +import { readTags } from './tags/read_tags'; interface ClientArgs { client: SavedObjectsClientContract; @@ -30,15 +32,19 @@ interface ClientArgs { interface GetCaseArgs extends ClientArgs { caseId: string; } + +interface GetCasesArgs extends ClientArgs { + options?: SavedObjectsFindOptionsType; +} interface GetCommentArgs extends ClientArgs { commentId: string; } interface PostCaseArgs extends ClientArgs { - attributes: NewCaseFormatted; + attributes: CaseAttributes; } interface PostCommentArgs extends ClientArgs { - attributes: NewCommentFormatted; + attributes: CommentAttributes; references: SavedObjectReference[]; } interface UpdateCaseArgs extends ClientArgs { @@ -61,15 +67,16 @@ interface CaseServiceDeps { export interface CaseServiceSetup { deleteCase(args: GetCaseArgs): Promise<{}>; deleteComment(args: GetCommentArgs): Promise<{}>; - getAllCases(args: ClientArgs): Promise; - getAllCaseComments(args: GetCaseArgs): Promise; - getCase(args: GetCaseArgs): Promise; - getComment(args: GetCommentArgs): Promise; + getAllCases(args: GetCasesArgs): Promise>; + getAllCaseComments(args: GetCaseArgs): Promise>; + getCase(args: GetCaseArgs): Promise>; + getComment(args: GetCommentArgs): Promise>; + getTags(args: ClientArgs): Promise; getUser(args: GetUserArgs): Promise; - postNewCase(args: PostCaseArgs): Promise; - postNewComment(args: PostCommentArgs): Promise; - updateCase(args: UpdateCaseArgs): Promise; - updateComment(args: UpdateCommentArgs): Promise; + postNewCase(args: PostCaseArgs): Promise>; + postNewComment(args: PostCommentArgs): Promise>; + updateCase(args: UpdateCaseArgs): Promise>; + updateComment(args: UpdateCommentArgs): Promise>; } export class CaseService { @@ -111,10 +118,10 @@ export class CaseService { throw error; } }, - getAllCases: async ({ client }: ClientArgs) => { + getAllCases: async ({ client, options }: GetCasesArgs) => { try { this.log.debug(`Attempting to GET all cases`); - return await client.find({ type: CASE_SAVED_OBJECT }); + return await client.find({ ...options, type: CASE_SAVED_OBJECT }); } catch (error) { this.log.debug(`Error on GET cases: ${error}`); throw error; @@ -132,6 +139,15 @@ export class CaseService { throw error; } }, + getTags: async ({ client }: ClientArgs) => { + try { + this.log.debug(`Attempting to GET all cases`); + return await readTags({ client }); + } catch (error) { + this.log.debug(`Error on GET cases: ${error}`); + throw error; + } + }, getUser: async ({ request, response }: GetUserArgs) => { let user; try { diff --git a/x-pack/plugins/case/server/services/tags/read_tags.ts b/x-pack/plugins/case/server/services/tags/read_tags.ts new file mode 100644 index 0000000000000..58ab99b164cfb --- /dev/null +++ b/x-pack/plugins/case/server/services/tags/read_tags.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { SavedObject, SavedObjectsClientContract } from 'kibana/server'; +import { CASE_SAVED_OBJECT } from '../../constants'; +import { CaseAttributes } from '../..'; + +const DEFAULT_PER_PAGE: number = 1000; + +export const convertToTags = (tagObjects: Array>): string[] => + tagObjects.reduce((accum, tagObj) => { + if (tagObj && tagObj.attributes && tagObj.attributes.tags) { + return [...accum, ...tagObj.attributes.tags]; + } else { + return accum; + } + }, []); + +export const convertTagsToSet = (tagObjects: Array>): Set => { + return new Set(convertToTags(tagObjects)); +}; + +// Note: This is doing an in-memory aggregation of the tags by calling each of the alerting +// records in batches of this const setting and uses the fields to try to get the least +// amount of data per record back. If saved objects at some point supports aggregations +// then this should be replaced with a an aggregation call. +// Ref: https://www.elastic.co/guide/en/kibana/master/saved-objects-api.html +export const readTags = async ({ + client, + perPage = DEFAULT_PER_PAGE, +}: { + client: SavedObjectsClientContract; + perPage?: number; +}): Promise => { + const tags = await readRawTags({ client, perPage }); + return tags; +}; + +export const readRawTags = async ({ + client, + perPage = DEFAULT_PER_PAGE, +}: { + client: SavedObjectsClientContract; + perPage?: number; +}): Promise => { + const firstTags = await client.find({ + type: CASE_SAVED_OBJECT, + fields: ['tags'], + page: 1, + perPage, + }); + const tags = await client.find({ + type: CASE_SAVED_OBJECT, + fields: ['tags'], + page: 1, + perPage: firstTags.total, + }); + + return Array.from(convertTagsToSet(tags.saved_objects)); +}; diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 8be1762133db6..65874ba3a461e 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -28,7 +28,6 @@ function getMockOptions(config: Partial = {}) { clusterClient: elasticsearchServiceMock.createClusterClient(), basePath: httpServiceMock.createSetupContract().basePath, loggers: loggingServiceMock.create(), - isSystemAPIRequest: jest.fn(), config: { session: { idleTimeout: null, lifespan: null }, authc: { providers: [], oidc: {}, saml: {} }, @@ -286,10 +285,11 @@ describe('Authenticator', () => { it('creates session whenever authentication provider returns state for system API requests', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'true' }, + }); const authorization = `Basic ${Buffer.from('foo:bar').toString('base64')}`; - mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user, { state: { authorization } }) ); @@ -307,10 +307,11 @@ describe('Authenticator', () => { it('creates session whenever authentication provider returns state for non-system API requests', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'false' }, + }); const authorization = `Basic ${Buffer.from('foo:bar').toString('base64')}`; - mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user, { state: { authorization } }) ); @@ -328,9 +329,10 @@ describe('Authenticator', () => { it('does not extend session for system API calls.', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'true' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user) ); @@ -346,9 +348,10 @@ describe('Authenticator', () => { it('extends session for non-system API calls.', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'false' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user) ); @@ -510,9 +513,10 @@ describe('Authenticator', () => { }); it('does not touch session for system API calls if authentication fails with non-401 reason.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'true' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(new Error('some error')) ); @@ -526,9 +530,10 @@ describe('Authenticator', () => { }); it('does not touch session for non-system API calls if authentication fails with non-401 reason.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'false' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(new Error('some error')) ); @@ -544,9 +549,10 @@ describe('Authenticator', () => { it('replaces existing session with the one returned by authentication provider for system API requests', async () => { const user = mockAuthenticatedUser(); const newState = { authorization: 'Basic yyy' }; - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'true' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user, { state: newState }) ); @@ -567,9 +573,10 @@ describe('Authenticator', () => { it('replaces existing session with the one returned by authentication provider for non-system API requests', async () => { const user = mockAuthenticatedUser(); const newState = { authorization: 'Basic yyy' }; - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'false' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user, { state: newState }) ); @@ -588,9 +595,10 @@ describe('Authenticator', () => { }); it('clears session if provider failed to authenticate system API request with 401 with active session.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'true' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(Boom.unauthorized()) ); @@ -604,9 +612,10 @@ describe('Authenticator', () => { }); it('clears session if provider failed to authenticate non-system API request with 401 with active session.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'false' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(Boom.unauthorized()) ); @@ -635,9 +644,10 @@ describe('Authenticator', () => { }); it('does not clear session if provider can not handle system API request authentication with active session.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'true' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); @@ -651,9 +661,10 @@ describe('Authenticator', () => { }); it('does not clear session if provider can not handle non-system API request authentication with active session.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'false' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); @@ -667,9 +678,10 @@ describe('Authenticator', () => { }); it('clears session for system API request if it belongs to not configured provider.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'true' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); @@ -683,9 +695,10 @@ describe('Authenticator', () => { }); it('clears session for non-system API request if it belongs to not configured provider.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'false' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index ea7792e902ec1..3ab49d3c5b124 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -88,7 +88,6 @@ export interface AuthenticatorOptions { loggers: LoggerFactory; clusterClient: IClusterClient; sessionStorageFactory: SessionStorageFactory; - isSystemAPIRequest: (request: KibanaRequest) => boolean; } // Mapping between provider key defined in the config and authentication @@ -310,7 +309,7 @@ export class Authenticator { this.updateSessionValue(sessionStorage, { providerType, - isSystemAPIRequest: this.options.isSystemAPIRequest(request), + isSystemRequest: request.isSystemRequest, authenticationResult, existingSession: ownsSession ? existingSession : null, }); @@ -434,12 +433,12 @@ export class Authenticator { providerType, authenticationResult, existingSession, - isSystemAPIRequest, + isSystemRequest, }: { providerType: string; authenticationResult: AuthenticationResult; existingSession: ProviderSession | null; - isSystemAPIRequest: boolean; + isSystemRequest: boolean; } ) { if (!existingSession && !authenticationResult.shouldUpdateState()) { @@ -451,7 +450,7 @@ export class Authenticator { // state we should store it in the session regardless of whether it's a system API request or not. const sessionCanBeUpdated = (authenticationResult.succeeded() || authenticationResult.redirected()) && - (authenticationResult.shouldUpdateState() || !isSystemAPIRequest); + (authenticationResult.shouldUpdateState() || !isSystemRequest); // If provider owned the session, but failed to authenticate anyway, that likely means that // session is not valid and we should clear it. Also provider can specifically ask to clear diff --git a/x-pack/plugins/security/server/authentication/index.test.ts b/x-pack/plugins/security/server/authentication/index.test.ts index d0de6d571b7a0..3727b1fc13dac 100644 --- a/x-pack/plugins/security/server/authentication/index.test.ts +++ b/x-pack/plugins/security/server/authentication/index.test.ts @@ -32,7 +32,6 @@ import { } from '../../../../../src/core/server'; import { AuthenticatedUser } from '../../common/model'; import { ConfigType, createConfig$ } from '../config'; -import { LegacyAPI } from '../plugin'; import { AuthenticationResult } from './authentication_result'; import { setupAuthentication } from '.'; import { @@ -47,7 +46,6 @@ describe('setupAuthentication()', () => { let mockSetupAuthenticationParams: { config: ConfigType; loggers: LoggerFactory; - getLegacyAPI(): Pick; http: jest.Mocked; clusterClient: jest.Mocked; license: jest.Mocked; @@ -73,7 +71,6 @@ describe('setupAuthentication()', () => { clusterClient: elasticsearchServiceMock.createClusterClient(), license: licenseMock.create(), loggers: loggingServiceMock.create(), - getLegacyAPI: jest.fn(), }; mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 4b73430ff13c4..467afe0034025 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -14,7 +14,6 @@ import { AuthenticatedUser } from '../../common/model'; import { ConfigType } from '../config'; import { getErrorStatusCode } from '../errors'; import { Authenticator, ProviderSession } from './authenticator'; -import { LegacyAPI } from '../plugin'; import { APIKeys, CreateAPIKeyParams, InvalidateAPIKeyParams } from './api_keys'; import { SecurityLicense } from '../../common/licensing'; @@ -36,7 +35,6 @@ interface SetupAuthenticationParams { config: ConfigType; license: SecurityLicense; loggers: LoggerFactory; - getLegacyAPI(): Pick; } export type Authentication = UnwrapPromise>; @@ -47,7 +45,6 @@ export async function setupAuthentication({ config, license, loggers, - getLegacyAPI, }: SetupAuthenticationParams) { const authLogger = loggers.get('authentication'); @@ -83,7 +80,6 @@ export async function setupAuthentication({ clusterClient, basePath: http.basePath, config: { session: config.session, authc: config.authc }, - isSystemAPIRequest: (request: KibanaRequest) => getLegacyAPI().isSystemAPIRequest(request), loggers, sessionStorageFactory: await http.createCookieSessionStorageFactory({ encryptionKey: config.encryptionKey, diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 5764418234739..328f2917fd550 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -9,7 +9,6 @@ import { first } from 'rxjs/operators'; import { ICustomClusterClient, CoreSetup, - KibanaRequest, Logger, PluginInitializerContext, RecursiveReadonly, @@ -40,7 +39,6 @@ export type FeaturesService = Pick; * to function properly. */ export interface LegacyAPI { - isSystemAPIRequest: (request: KibanaRequest) => boolean; auditLogger: { log: (eventType: string, message: string, data?: Record) => void; }; @@ -133,7 +131,6 @@ export class Plugin { config, license, loggers: this.initializerContext.logger, - getLegacyAPI: this.getLegacyAPI, }); const authz = await setupAuthorization({ diff --git a/x-pack/plugins/siem/server/config.ts b/x-pack/plugins/siem/server/config.ts index 456646cc825f3..224043c0c6fe5 100644 --- a/x-pack/plugins/siem/server/config.ts +++ b/x-pack/plugins/siem/server/config.ts @@ -6,7 +6,7 @@ import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext } from '../../../../src/core/server'; import { SIGNALS_INDEX_KEY, DEFAULT_SIGNALS_INDEX, diff --git a/x-pack/plugins/siem/server/index.ts b/x-pack/plugins/siem/server/index.ts index c675be691b47e..83e2f900a3b90 100644 --- a/x-pack/plugins/siem/server/index.ts +++ b/x-pack/plugins/siem/server/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext } from '../../../../src/core/server'; import { Plugin } from './plugin'; import { configSchema, ConfigType } from './config'; diff --git a/x-pack/plugins/siem/server/plugin.ts b/x-pack/plugins/siem/server/plugin.ts index 866f4d7575e2f..ccc6aef1452b2 100644 --- a/x-pack/plugins/siem/server/plugin.ts +++ b/x-pack/plugins/siem/server/plugin.ts @@ -6,7 +6,7 @@ import { Observable } from 'rxjs'; -import { CoreSetup, PluginInitializerContext, Logger } from 'src/core/server'; +import { CoreSetup, PluginInitializerContext, Logger } from '../../../../src/core/server'; import { createConfig$, ConfigType } from './config'; export class Plugin { diff --git a/x-pack/test/api_integration/apis/security/basic_login.js b/x-pack/test/api_integration/apis/security/basic_login.js index d4b41603944f6..9b6f49a9a916b 100644 --- a/x-pack/test/api_integration/apis/security/basic_login.js +++ b/x-pack/test/api_integration/apis/security/basic_login.js @@ -201,7 +201,7 @@ export default function({ getService }) { const systemAPIResponse = await supertest .get('/internal/security/me') .set('kbn-xsrf', 'xxx') - .set('kbn-system-api', 'true') + .set('kbn-system-request', 'true') .set('Cookie', sessionCookie.cookieString()) .expect(200); diff --git a/x-pack/test/api_integration/apis/security/session.ts b/x-pack/test/api_integration/apis/security/session.ts index 5d0935bb1ae2d..d819dd38dddb1 100644 --- a/x-pack/test/api_integration/apis/security/session.ts +++ b/x-pack/test/api_integration/apis/security/session.ts @@ -28,7 +28,7 @@ export default function({ getService }: FtrProviderContext) { supertest .get('/internal/security/session') .set('kbn-xsrf', 'xxx') - .set('kbn-system-api', 'true') + .set('kbn-system-request', 'true') .set('Cookie', sessionCookie.cookieString()) .send() .expect(200); diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index 6efaae70e089b..8f902471cf6cd 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -178,7 +178,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { expect(navLinks).to.eql(['Discover', 'Stack Management']); }); - it(`does not allow navigation to advanced settings; redirects to management home`, async () => { + // https://github.com/elastic/kibana/issues/57377 + it.skip(`does not allow navigation to advanced settings; redirects to management home`, async () => { await PageObjects.common.navigateToActualUrl('kibana', 'management/kibana/settings', { ensureCurrentUrl: false, shouldLoginIfPrompted: false, 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 097812c576e92..aceebf7219c3f 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 @@ -13,7 +13,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); - describe('spaces feature controls', () => { + // FLAKY: https://github.com/elastic/kibana/issues/57377 + describe.skip('spaces feature controls', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); }); @@ -57,7 +58,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('space with Advanced Settings disabled', () => { + // https://github.com/elastic/kibana/issues/57413 + describe.skip('space with Advanced Settings disabled', () => { before(async () => { // we need to load the following in every situation as deleting // a space deletes all of the associated saved objects diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts index 6a6e2f23785e3..ad09bc5c89143 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -21,7 +21,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const queryBar = getService('queryBar'); const savedQueryManagementComponent = getService('savedQueryManagementComponent'); - describe('dashboard security', () => { + // FLAKY: https://github.com/elastic/kibana/issues/44631 + describe.skip('dashboard security', () => { before(async () => { await esArchiver.load('dashboard/feature_controls/security'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 1796858165a2b..8669577f202a7 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -27,7 +27,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); } - describe('security', () => { + // FLAKY: https://github.com/elastic/kibana/issues/45348 + describe.skip('security', () => { before(async () => { await esArchiver.load('discover/feature_controls/security'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts index 75020d6eab7e4..b303ad23f8977 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts @@ -52,7 +52,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('space with Index Patterns disabled', () => { + // https://github.com/elastic/kibana/issues/57601 + describe.skip('space with Index Patterns disabled', () => { before(async () => { // we need to load the following in every situation as deleting // a space deletes all of the associated saved objects diff --git a/x-pack/test/functional/apps/rollup_job/tsvb.js b/x-pack/test/functional/apps/rollup_job/tsvb.js index f3782c4c91644..b697e751ef550 100644 --- a/x-pack/test/functional/apps/rollup_job/tsvb.js +++ b/x-pack/test/functional/apps/rollup_job/tsvb.js @@ -21,7 +21,8 @@ export default function({ getService, getPageObjects }) { 'timePicker', ]); - describe('tsvb integration', function() { + // https://github.com/elastic/kibana/issues/56816 + describe.skip('tsvb integration', function() { //Since rollups can only be created once with the same name (even if you delete it), //we add the Date.now() to avoid name collision if you run the tests locally back to back. const rollupJobName = `tsvb-test-rollup-job-${Date.now()}`; diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index bdcdc4b7cd3ec..6db8ad28deccb 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -24,7 +24,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const queryBar = getService('queryBar'); const savedQueryManagementComponent = getService('savedQueryManagementComponent'); - describe('feature controls security', () => { + // FLAKY: https://github.com/elastic/kibana/issues/50018 + describe.skip('feature controls security', () => { before(async () => { await esArchiver.load('visualize/default'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index 570d7026cf99e..55853f8b0fbde 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -199,7 +199,7 @@ export default function({ getService }: FtrProviderContext) { const systemAPIResponse = await supertest .get('/internal/security/me') .set('kbn-xsrf', 'xxx') - .set('kbn-system-api', 'true') + .set('kbn-system-request', 'true') .set('Cookie', sessionCookie.cookieString()) .expect(200); diff --git a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js index 094537fd61436..abb65e46263ab 100644 --- a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js +++ b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js @@ -285,7 +285,7 @@ export default function({ getService }) { const systemAPIResponse = await supertest .get('/internal/security/me') .set('kbn-xsrf', 'xxx') - .set('kbn-system-api', 'true') + .set('kbn-system-request', 'true') .set('Cookie', sessionCookie.cookieString()) .expect(200); diff --git a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts index 1ae7488fcf379..6cb92585de36e 100644 --- a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts +++ b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts @@ -242,7 +242,7 @@ export default function({ getService }: FtrProviderContext) { .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('kbn-xsrf', 'xxx') - .set('kbn-system-api', 'true') + .set('kbn-system-request', 'true') .set('Cookie', sessionCookie.cookieString()) .expect(200); diff --git a/x-pack/test/saml_api_integration/apis/security/saml_login.ts b/x-pack/test/saml_api_integration/apis/security/saml_login.ts index 610850cfb00bb..b8296aa703607 100644 --- a/x-pack/test/saml_api_integration/apis/security/saml_login.ts +++ b/x-pack/test/saml_api_integration/apis/security/saml_login.ts @@ -330,7 +330,7 @@ export default function({ getService }: FtrProviderContext) { const systemAPIResponse = await supertest .get('/internal/security/me') .set('kbn-xsrf', 'xxx') - .set('kbn-system-api', 'true') + .set('kbn-system-request', 'true') .set('Cookie', sessionCookie.cookieString()) .expect(200);
+ Test supplement +
{'Test supplement'}
+ {`${data.created_by.username}`} + {` ${i18n.ADDED_DESCRIPTION} `}{' '} + + {/* STEPH FIX come back and add label `on` */} +
{i18n.NO_TAGS}
+ + {username} + +