From 31298d0422903a5d3bd76c96b6c413ec1890fbf9 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 8 Jul 2021 10:49:48 -0400 Subject: [PATCH 01/77] [Lens] Fix dashboard error when localstorage is empty (#104788) --- .../indexpattern_datasource/loader.test.ts | 51 +++++++++++++++++++ .../public/indexpattern_datasource/loader.ts | 17 ++++--- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index d58d9e593e101d..5eb530e50f1907 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -497,6 +497,57 @@ describe('loader', () => { }); }); + it('should initialize all the embeddable references without local storage', async () => { + const savedState: IndexPatternPersistedState = { + layers: { + layerb: { + columnOrder: ['col1', 'col2'], + columns: { + col1: { + dataType: 'date', + isBucketed: true, + label: 'My date', + operationType: 'date_histogram', + params: { + interval: 'm', + }, + sourceField: 'timestamp', + }, + col2: { + dataType: 'number', + isBucketed: false, + label: 'Sum of bytes', + operationType: 'sum', + sourceField: 'bytes', + }, + }, + }, + }, + }; + const storage = createMockStorage({}); + const state = await loadInitialState({ + persistedState: savedState, + references: [ + { name: 'indexpattern-datasource-current-indexpattern', id: '2', type: 'index-pattern' }, + { name: 'indexpattern-datasource-layer-layerb', id: '2', type: 'index-pattern' }, + { name: 'another-reference', id: 'c', type: 'index-pattern' }, + ], + indexPatternsService: mockIndexPatternsService(), + storage, + options: { isFullEditor: false }, + }); + + expect(state).toMatchObject({ + currentIndexPatternId: undefined, + indexPatternRefs: [], + indexPatterns: { + '2': sampleIndexPatterns['2'], + }, + layers: { layerb: { ...savedState.layers.layerb, indexPatternId: '2' } }, + }); + expect(storage.set).not.toHaveBeenCalled(); + }); + it('should initialize from saved state', async () => { const savedState: IndexPatternPersistedState = { layers: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 82c27a76bb4833..3db17a94d58b54 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -227,12 +227,14 @@ export async function loadInitialState({ const state = persistedState && references ? injectReferences(persistedState, references) : undefined; + const fallbackId = lastUsedIndexPatternId || defaultIndexPatternId || indexPatternRefs[0]?.id; + const requiredPatterns: string[] = uniq( state ? Object.values(state.layers) .map((l) => l.indexPatternId) .concat(state.currentIndexPatternId) - : [lastUsedIndexPatternId || defaultIndexPatternId || indexPatternRefs[0]?.id] + : [fallbackId] ) // take out the undefined from the list .filter(Boolean); @@ -248,15 +250,16 @@ export async function loadInitialState({ indexPatternRefs[0]?.id, ].filter((id) => id != null && availableIndexPatterns.has(id)); - const currentIndexPatternId = availableIndexPatternIds[0]!; + const currentIndexPatternId = availableIndexPatternIds[0]; if (currentIndexPatternId) { setLastUsedIndexPatternId(storage, currentIndexPatternId); - } - if (!requiredPatterns.includes(currentIndexPatternId)) { - requiredPatterns.push(currentIndexPatternId); + if (!requiredPatterns.includes(currentIndexPatternId)) { + requiredPatterns.push(currentIndexPatternId); + } } + const indexPatterns = await loadIndexPatterns({ indexPatternsService, cache: {}, @@ -265,7 +268,7 @@ export async function loadInitialState({ if (state) { return { ...state, - currentIndexPatternId, + currentIndexPatternId: currentIndexPatternId ?? fallbackId, indexPatternRefs, indexPatterns, existingFields: {}, @@ -274,7 +277,7 @@ export async function loadInitialState({ } return { - currentIndexPatternId, + currentIndexPatternId: currentIndexPatternId ?? fallbackId, indexPatternRefs, indexPatterns, layers: {}, From 80ab2ff36de6554bd8a5f3a62bc18b971ea057a6 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Thu, 8 Jul 2021 16:01:23 +0100 Subject: [PATCH 02/77] [ML] Transforms: Fix page layout on resize and link button widths (#104680) --- .../clone_transform_section.tsx | 5 ++- .../step_create/step_create_form.tsx | 11 +++--- .../components/wizard/_wizard.scss | 21 +++-------- .../components/wizard/wizard.tsx | 35 +------------------ .../create_transform_section.tsx | 5 ++- 5 files changed, 19 insertions(+), 58 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx index 8aecf403186c5c..91df8d5eb4fbed 100644 --- a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx @@ -131,7 +131,10 @@ export const CloneTransformSection: FC = ({ match, location }) => { - + {typeof errorMessage !== 'undefined' && ( <> = React.memo( {created && ( - - + + } title={i18n.translate('xpack.transform.stepCreateForm.transformListCardTitle', { @@ -498,7 +497,7 @@ export const StepCreateForm: FC = React.memo( /> {started === true && createIndexPattern === true && indexPatternId === undefined && ( - + @@ -515,7 +514,7 @@ export const StepCreateForm: FC = React.memo( )} {isDiscoverAvailable && discoverLink !== undefined && ( - + } title={i18n.translate('xpack.transform.stepCreateForm.discoverCardTitle', { @@ -532,7 +531,7 @@ export const StepCreateForm: FC = React.memo( /> )} - + )} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/_wizard.scss b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/_wizard.scss index 1b493e9e744907..2fb415f8ab2d96 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/_wizard.scss +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/_wizard.scss @@ -8,22 +8,11 @@ } } -.transform__stepDefineForm { - align-items: flex-start; -} - -.transform__stepDefineFormLeftColumn { - min-width: 420px; - border-right: 1px solid $euiColorLightShade; -} - /* -This is an override to replicate the previous full-page-width of the transforms creation wizard -when it was in use within the ML plugin. The Kibana management section limits a max-width to 1200px -which is a bit narrow for the two column layout of the transform wizard. We might revisit this for -future versions to blend in more with the overall design of the Kibana management section. -The management section's navigation width is 192px + 24px right margin +This ensures the wizard goes full page width, and that the data grid in the page does not +cause the body of the wizard page to overflow into the side navigation of the Kibana +Stack Management page on resize. */ -.mgtPage__body--transformWizard { - max-width: calc(100% - 216px); +.transform__wizardBody { + max-width: calc(100% - 16px); } diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx index 5ae464affa0164..63e21e5d8aa14e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, FC, useEffect, useRef, useState, createContext, useMemo } from 'react'; +import React, { Fragment, FC, useRef, useState, createContext, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; @@ -34,11 +34,6 @@ import { WizardNav } from '../wizard_nav'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; import type { RuntimeMappings } from '../step_define/common/types'; -enum KBN_MANAGEMENT_PAGE_CLASSNAME { - DEFAULT_BODY = 'mgtPage__body', - TRANSFORM_BODY_MODIFIER = 'mgtPage__body--transformWizard', -} - enum WIZARD_STEPS { DEFINE, DETAILS, @@ -121,34 +116,6 @@ export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) // The CREATE state const [stepCreateState, setStepCreateState] = useState(getDefaultStepCreateState); - useEffect(() => { - // The transform plugin doesn't control the wrapping management page via React - // so we use plain JS to add and remove a custom CSS class to set the full - // page width to 100% for the transform wizard. It's done to replicate the layout - // as it was when transforms were part of the ML plugin. This will be revisited - // to come up with an approach that's more in line with the overall layout - // of the Kibana management section. - let managementBody = document.getElementsByClassName( - KBN_MANAGEMENT_PAGE_CLASSNAME.DEFAULT_BODY - ); - - if (managementBody.length > 0) { - managementBody[0].classList.replace( - KBN_MANAGEMENT_PAGE_CLASSNAME.DEFAULT_BODY, - KBN_MANAGEMENT_PAGE_CLASSNAME.TRANSFORM_BODY_MODIFIER - ); - return () => { - managementBody = document.getElementsByClassName( - KBN_MANAGEMENT_PAGE_CLASSNAME.TRANSFORM_BODY_MODIFIER - ); - managementBody[0].classList.replace( - KBN_MANAGEMENT_PAGE_CLASSNAME.TRANSFORM_BODY_MODIFIER, - KBN_MANAGEMENT_PAGE_CLASSNAME.DEFAULT_BODY - ); - }; - } - }, []); - const transformConfig = getCreateTransformRequestBody( indexPattern.title, stepDefineState, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx index d736bd60f2df64..4cb9ec926c0491 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx @@ -68,7 +68,10 @@ export const CreateTransformSection: FC = ({ match }) => { - + {searchItemsError !== undefined && ( <> From 9744de2a1a8de06238c4c0ec5b210e87e8c60494 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 8 Jul 2021 08:16:49 -0700 Subject: [PATCH 03/77] [Workplace Search] Fix download diagnostics button (#104792) * EnterpriseSearchRequestHandler: Create new hasJsonResponse param - in place of try/catch previously added for an empty response - we should instead attempt to handle all non-JSON responses manually, as they're (currently) the outlier * Update AS sample engine/onboarding route with hasJsonResponse: false - as this was the original route that added the try/catch - should still work without it now * Update WS source download diagnostic route with hasJsonResponse: false - this was the original impetus for the non-JSON feature request - WS needs the ability to download JSON files directly * Update Download Diagnostics filename to match standalone UI * Fix misc routing issues in Source Settings - `/api` route is missing Kibana basepaths (affects both dev modes and users who have Kibana canonically set to a non-root URL) - We shouldn't be using RR's native , but instead our RR helpers which use Kibana's instance of RR history --- .../components/source_settings.test.tsx | 10 +++++++++ .../components/source_settings.tsx | 18 ++++++++------- .../enterprise_search_request_handler.test.ts | 22 ++++++++++++------- .../lib/enterprise_search_request_handler.ts | 14 ++++++++---- .../routes/app_search/onboarding.test.ts | 1 + .../server/routes/app_search/onboarding.ts | 1 + .../routes/workplace_search/sources.test.ts | 2 ++ .../server/routes/workplace_search/sources.ts | 2 ++ 8 files changed, 50 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx index 74f32cc22c2a8c..da4346d54727c8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx @@ -125,5 +125,15 @@ describe('SourceSettings', () => { '/api/workplace_search/account/sources/123/download_diagnostics' ); }); + + it('renders with the correct download file name', () => { + jest.spyOn(global.Date, 'now').mockImplementationOnce(() => new Date('1970-01-01').valueOf()); + + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="DownloadDiagnosticsButton"]').prop('download')).toEqual( + '123_custom_0_diagnostics.json' + ); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index 667e7fd4dbfb42..e4f52d94ad9e72 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -6,14 +6,12 @@ */ import React, { useEffect, useState, ChangeEvent, FormEvent } from 'react'; -import { Link } from 'react-router-dom'; import { useActions, useValues } from 'kea'; import { isEmpty } from 'lodash'; import { EuiButton, - EuiButtonEmpty, EuiConfirmModal, EuiFieldText, EuiFlexGroup, @@ -22,6 +20,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { HttpLogic } from '../../../../shared/http'; +import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; import { AppLogic } from '../../../app_logic'; import { ContentSection } from '../../../components/shared/content_section'; import { SourceConfigFields } from '../../../components/shared/source_config_fields'; @@ -57,6 +57,8 @@ import { SourceLogic } from '../source_logic'; import { SourceLayout } from './source_layout'; export const SourceSettings: React.FC = () => { + const { http } = useValues(HttpLogic); + const { updateContentSource, removeContentSource } = useActions(SourceLogic); const { getSourceConfigData } = useActions(AddSourceLogic); @@ -90,8 +92,8 @@ export const SourceSettings: React.FC = () => { const { clientId, clientSecret, publicKey, consumerKey, baseUrl } = configuredFields || {}; const diagnosticsPath = isOrganization - ? `/api/workplace_search/org/sources/${id}/download_diagnostics` - : `/api/workplace_search/account/sources/${id}/download_diagnostics`; + ? http.basePath.prepend(`/api/workplace_search/org/sources/${id}/download_diagnostics`) + : http.basePath.prepend(`/api/workplace_search/account/sources/${id}/download_diagnostics`); const handleNameChange = (e: ChangeEvent) => setValue(e.target.value); @@ -172,9 +174,9 @@ export const SourceSettings: React.FC = () => { baseUrl={baseUrl} /> - - {SOURCE_CONFIG_LINK} - + + {SOURCE_CONFIG_LINK} + )} @@ -184,7 +186,7 @@ export const SourceSettings: React.FC = () => { href={diagnosticsPath} isLoading={buttonLoading} data-test-subj="DownloadDiagnosticsButton" - download + download={`${id}_${serviceType}_${Date.now()}_diagnostics.json`} > {SYNC_DIAGNOSTICS_BUTTON} diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts index e6524151b0a6cd..d0e74f3234c146 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts @@ -209,17 +209,23 @@ describe('EnterpriseSearchRequestHandler', () => { headers: mockExpectedResponseHeaders, }); }); - }); - it('works if response contains no json data', async () => { - EnterpriseSearchAPI.mockReturn(); + it('passes back the response body as-is if hasJsonResponse is false', async () => { + const mockFile = new File(['mockFile'], 'mockFile.json'); + EnterpriseSearchAPI.mockReturn(mockFile); - const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/prep' }); - await makeAPICall(requestHandler); + const requestHandler = enterpriseSearchRequestHandler.createRequest({ + path: '/api/file', + hasJsonResponse: false, + }); + await makeAPICall(requestHandler); - expect(responseMock.custom).toHaveBeenCalledWith({ - statusCode: 200, - headers: mockExpectedResponseHeaders, + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/file'); + expect(responseMock.custom).toHaveBeenCalledWith({ + body: expect.any(Buffer), // Unfortunately Response() buffers the body so we can't actually inspect/equality assert on it + statusCode: 200, + headers: mockExpectedResponseHeaders, + }); }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts index b4768c1a9ee150..8031fc724f7b37 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts @@ -34,6 +34,7 @@ interface ConstructorDependencies { interface RequestParams { path: string; params?: object; + hasJsonResponse?: boolean; hasValidData?: Function; } interface ErrorResponse { @@ -64,7 +65,12 @@ export class EnterpriseSearchRequestHandler { this.enterpriseSearchUrl = config.host as string; } - createRequest({ path, params = {}, hasValidData = () => true }: RequestParams) { + createRequest({ + path, + params = {}, + hasJsonResponse = true, + hasValidData = () => true, + }: RequestParams) { return async ( _context: RequestHandlerContext, request: KibanaRequest, @@ -119,7 +125,7 @@ export class EnterpriseSearchRequestHandler { // Check returned data let responseBody; - try { + if (hasJsonResponse) { const json = await apiResponse.json(); if (!hasValidData(json)) { @@ -134,8 +140,8 @@ export class EnterpriseSearchRequestHandler { } else { responseBody = json; } - } catch (e) { - responseBody = undefined; + } else { + responseBody = apiResponse.body; } // Pass successful responses back to the front-end diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/onboarding.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/onboarding.test.ts index c26f8dbaf52137..47b480f61341a6 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/onboarding.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/onboarding.test.ts @@ -30,6 +30,7 @@ describe('engine routes', () => { mockRouter.callRoute({ body: {} }); expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ path: '/as/onboarding/complete', + hasJsonResponse: false, }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/onboarding.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/onboarding.ts index 9a46c75555969b..147f935a56aed9 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/onboarding.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/onboarding.ts @@ -24,6 +24,7 @@ export function registerOnboardingRoutes({ }, enterpriseSearchRequestHandler.createRequest({ path: '/as/onboarding/complete', + hasJsonResponse: false, }) ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts index 4043f9daddaa76..a68a7716933f80 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts @@ -559,6 +559,7 @@ describe('sources routes', () => { it('creates a request handler', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ path: '/ws/sources/:sourceId/download_diagnostics', + hasJsonResponse: false, }); }); }); @@ -1057,6 +1058,7 @@ describe('sources routes', () => { it('creates a request handler', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ path: '/ws/org/sources/:sourceId/download_diagnostics', + hasJsonResponse: false, }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index b393ab9d1f26a2..5de4387f2c0d9c 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -400,6 +400,7 @@ export function registerAccountSourceDownloadDiagnosticsRoute({ }, enterpriseSearchRequestHandler.createRequest({ path: '/ws/sources/:sourceId/download_diagnostics', + hasJsonResponse: false, }) ); } @@ -748,6 +749,7 @@ export function registerOrgSourceDownloadDiagnosticsRoute({ }, enterpriseSearchRequestHandler.createRequest({ path: '/ws/org/sources/:sourceId/download_diagnostics', + hasJsonResponse: false, }) ); } From 030f77b089b53bf0b014942e0dae6da960ff83e6 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 8 Jul 2021 17:34:20 +0200 Subject: [PATCH 04/77] remove previous time range mode (#104830) --- docs/user/dashboard/lens-advanced.asciidoc | 3 --- .../public/indexpattern_datasource/time_shift_utils.tsx | 6 ------ 2 files changed, 9 deletions(-) diff --git a/docs/user/dashboard/lens-advanced.asciidoc b/docs/user/dashboard/lens-advanced.asciidoc index 33e0e362058f43..374d36e7586c4a 100644 --- a/docs/user/dashboard/lens-advanced.asciidoc +++ b/docs/user/dashboard/lens-advanced.asciidoc @@ -301,9 +301,6 @@ image::images/lens_advanced_5_2.png[Line chart with cumulative sum of orders mad *Lens* allows you to compare the currently selected time range with historical data using the *Time shift* option. To calculate the percent change, use *Formula*. -Time shifts can be used on any metric. The special shift *previous* will show the time window preceding the currently selected one, spanning the same duration. -For example, if *Last 7 days* is selected in the time filter, *previous* will show data from 14 days ago to 7 days ago. - If multiple time shifts are used in a single chart, a multiple of the date histogram interval should be chosen - otherwise data points might not line up in the chart and empty spots can occur. For example, if a daily interval is used, shifting one series by *36h*, and another one by *1d*, is not recommended. In this scenario, either reduce the interval to *12h*, or create two separate charts. diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx index a1bc643c3bd932..8cfd25914f59c4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx @@ -81,12 +81,6 @@ export const timeShiftOptions = [ }), value: '1y', }, - { - label: i18n.translate('xpack.lens.indexPattern.timeShift.previous', { - defaultMessage: 'Previous time range', - }), - value: 'previous', - }, ]; export const timeShiftOptionOrder = timeShiftOptions.reduce<{ [key: string]: number }>( From a1e3d027531b9877d7916a732a744faf681c02fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 8 Jul 2021 11:42:09 -0400 Subject: [PATCH 05/77] [APM] Fixing visual bugs in APM Schema settings (#104491) * small style adjustments * show loading page while migrating * adding storybook * addressing PR comments --- .../Settings/schema/confirm_switch_modal.tsx | 4 - .../components/app/Settings/schema/index.tsx | 49 +++++++++--- .../schema/migration_in_progress_panel.tsx | 36 +++++++++ .../app/Settings/schema/schema.stories.tsx | 76 ++++++++++++++++++- .../app/Settings/schema/schema_overview.tsx | 41 ++++------ 5 files changed, 161 insertions(+), 45 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/Settings/schema/migration_in_progress_panel.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx index 9900093253d2a4..04817aaf84f3ef 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx @@ -21,13 +21,11 @@ interface Props { onConfirm: () => void; onCancel: () => void; unsupportedConfigs: Array<{ key: string; value: string }>; - isLoading: boolean; } export function ConfirmSwitchModal({ onConfirm, onCancel, unsupportedConfigs, - isLoading, }: Props) { const [isConfirmChecked, setIsConfirmChecked] = useState(false); const hasUnsupportedConfigs = !!unsupportedConfigs.length; @@ -52,7 +50,6 @@ export function ConfirmSwitchModal({ defaultFocusedButton="confirm" onConfirm={onConfirm} confirmButtonDisabled={!isConfirmChecked} - isLoading={isLoading} >

{i18n.translate('xpack.apm.settings.schema.confirm.descriptionText', { @@ -135,7 +132,6 @@ export function ConfirmSwitchModal({ onChange={(e) => { setIsConfirmChecked(e.target.checked); }} - disabled={isLoading} />

diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx index fee072470f05a3..5a67ce28e9e1a2 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx @@ -8,6 +8,8 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'kibana/public'; +import moment from 'moment'; +import { useLocalStorage } from '../../../../hooks/useLocalStorage'; import { SchemaOverview } from './schema_overview'; import { ConfirmSwitchModal } from './confirm_switch_modal'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; @@ -19,10 +21,22 @@ import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plug type FleetMigrationCheckResponse = APIReturnType<'GET /api/apm/fleet/migration_check'>; +const APM_DATA_STREAMS_MIGRATION_STATUS_LS = { + value: '', + expiry: '', +}; + export function Schema() { + const [ + apmDataStreamsMigrationStatus, + setApmDataStreamsMigrationStatus, + ] = useLocalStorage( + 'apm.dataStreamsMigrationStatus', + APM_DATA_STREAMS_MIGRATION_STATUS_LS + ); + const { toasts } = useApmPluginContext().core.notifications; const [isSwitchActive, setIsSwitchActive] = useState(false); - const [isLoadingMigration, setIsLoadingMigration] = useState(false); const [isLoadingConfirmation, setIsLoadingConfirmation] = useState(false); const [unsupportedConfigs, setUnsupportedConfigs] = useState< Array<{ key: string; value: any }> @@ -42,6 +56,19 @@ export function Schema() { const hasCloudAgentPolicy = !!data.has_cloud_agent_policy; const hasCloudApmPackagePolicy = !!data.has_cloud_apm_package_policy; const hasRequiredRole = !!data.has_required_role; + + function updateLocalStorage(newStatus: FETCH_STATUS) { + setApmDataStreamsMigrationStatus({ + value: newStatus, + expiry: moment().add(5, 'minutes').toISOString(), + }); + } + + const { value: localStorageValue, expiry } = apmDataStreamsMigrationStatus; + const isMigrating = + localStorageValue === FETCH_STATUS.LOADING && + moment(expiry).valueOf() > moment.now(); + return ( <> {isSwitchActive && ( { - setIsLoadingMigration(true); - const apmPackagePolicy = await createCloudApmPackagePolicy(toasts); + setIsSwitchActive(false); + const apmPackagePolicy = await createCloudApmPackagePolicy( + toasts, + updateLocalStorage + ); if (!apmPackagePolicy) { - setIsLoadingMigration(false); return; } - setIsSwitchActive(false); refetch(); }} onCancel={() => { - if (isLoadingMigration) { - return; - } setIsSwitchActive(false); }} unsupportedConfigs={unsupportedConfigs} @@ -112,8 +137,10 @@ async function getUnsupportedApmServerConfigs( } async function createCloudApmPackagePolicy( - toasts: NotificationsStart['toasts'] + toasts: NotificationsStart['toasts'], + updateLocalStorage: (status: FETCH_STATUS) => void ) { + updateLocalStorage(FETCH_STATUS.LOADING); try { const { cloud_apm_package_policy: cloudApmPackagePolicy, @@ -121,8 +148,10 @@ async function createCloudApmPackagePolicy( endpoint: 'POST /api/apm/fleet/cloud_apm_package_policy', signal: null, }); + updateLocalStorage(FETCH_STATUS.SUCCESS); return cloudApmPackagePolicy; } catch (error) { + updateLocalStorage(FETCH_STATUS.FAILURE); toasts.addDanger({ title: i18n.translate( 'xpack.apm.settings.createApmPackagePolicy.errorToast.title', diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/migration_in_progress_panel.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/migration_in_progress_panel.tsx new file mode 100644 index 00000000000000..854d1dd823d234 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/migration_in_progress_panel.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { EuiCard } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export function MigrationInProgressPanel() { + return ( + + + } + title={i18n.translate( + 'xpack.apm.settings.schema.migrationInProgressPanelTitle', + { defaultMessage: 'Switching to data streams...' } + )} + description={i18n.translate( + 'xpack.apm.settings.schema.migrationInProgressPanelDescription', + { + defaultMessage: + "We're now creating a Fleet Server instance to contain the new APM Server while shutting down the old APM server instance. Within minutes you should see your data pour into the app again.", + } + )} + /> + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/schema.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/schema.stories.tsx index 7cac4ba97e7236..b22260ffabe463 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/schema.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/schema.stories.tsx @@ -5,31 +5,99 @@ * 2.0. */ -import type { Story } from '@storybook/react'; +import type { Meta, Story } from '@storybook/react'; import React, { ComponentType } from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { CoreStart } from '../../../../../../../../src/core/public'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { createCallApmApi } from '../../../../services/rest/createCallApmApi'; import { Schema } from './'; +interface Args { + hasCloudAgentPolicy: boolean; + hasCloudApmPackagePolicy: boolean; + cloudApmMigrationEnabled: boolean; + hasRequiredRole: boolean; + isMigrating: boolean; +} + export default { title: 'app/Settings/Schema', component: Schema, + argTypes: { + hasCloudAgentPolicy: { + control: { + type: 'boolean', + options: [true, false], + defaultValue: true, + }, + }, + hasCloudApmPackagePolicy: { + control: { + type: 'boolean', + options: [true, false], + defaultValue: false, + }, + }, + cloudApmMigrationEnabled: { + control: { + type: 'boolean', + options: [true, false], + defaultValue: true, + }, + }, + hasRequiredRole: { + control: { + type: 'boolean', + options: [true, false], + defaultValue: true, + }, + }, + isMigrating: { + control: { + type: 'boolean', + options: [true, false], + defaultValue: false, + }, + }, + }, decorators: [ - (StoryComponent: ComponentType) => { + (StoryComponent: ComponentType, { args }: Meta) => { + if (args?.isMigrating) { + const expiryDate = new Date(); + expiryDate.setMinutes(expiryDate.getMinutes() + 5); + window.localStorage.setItem( + 'apm.dataStreamsMigrationStatus', + JSON.stringify({ + value: 'loading', + expiry: expiryDate.toISOString(), + }) + ); + } else { + window.localStorage.removeItem('apm.dataStreamsMigrationStatus'); + } const coreMock = ({ http: { + basePath: { prepend: () => {} }, get: () => { - return {}; + return { + has_cloud_agent_policy: args?.hasCloudAgentPolicy, + has_cloud_apm_package_policy: args?.hasCloudApmPackagePolicy, + cloud_apm_migration_enabled: args?.cloudApmMigrationEnabled, + has_required_role: args?.hasRequiredRole, + }; }, }, + uiSettings: { get: () => '' }, } as unknown) as CoreStart; createCallApmApi(coreMock); return ( - + + + ); }, diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx index 7a874ed5b8037f..a9a0b824cc8289 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx @@ -7,7 +7,6 @@ import { EuiButton, - EuiCallOut, EuiCard, EuiFlexGroup, EuiFlexItem, @@ -24,9 +23,11 @@ import { APMLink } from '../../../shared/Links/apm/APMLink'; import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink'; import { useFleetCloudAgentPolicyHref } from '../../../shared/Links/kibana'; import rocketLaunchGraphic from './blog-rocket-720x420.png'; +import { MigrationInProgressPanel } from './migration_in_progress_panel'; interface Props { onSwitch: () => void; + isMigrating: boolean; isMigrated: boolean; isLoading: boolean; isLoadingConfirmation: boolean; @@ -36,6 +37,7 @@ interface Props { } export function SchemaOverview({ onSwitch, + isMigrating, isMigrated, isLoading, isLoadingConfirmation, @@ -58,6 +60,15 @@ export function SchemaOverview({ ); } + if (isMigrating && !isMigrated) { + return ( + <> + + + + ); + } + if (isMigrated) { return ( <> @@ -125,7 +136,7 @@ export function SchemaOverview({ - + } title={i18n.translate( @@ -154,7 +165,7 @@ export function SchemaOverview({ } /> - + - - - - - -

- {i18n.translate( - 'xpack.apm.settings.schema.migrate.calloutNote.message', - { - defaultMessage: - 'If you have custom dashboards, machine learning jobs, or source maps that use classic APM indices, you must reconfigure them for data streams.', - } - )} -

-
-
- -
); } From db048c33a5194a78c2dd95ab18a79aff87b5c49c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Thu, 8 Jul 2021 17:47:02 +0200 Subject: [PATCH 06/77] [ML] Changes DFA links in doc links service. (#104871) --- src/core/public/doc_links/doc_links_service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 2187d5a0a33be0..7a843a41cc4ca6 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -244,14 +244,14 @@ export class DocLinksService { anomalyDetectionJobTips: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/create-jobs.html#job-tips`, anomalyDetectionModelMemoryLimits: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/create-jobs.html#model-memory-limits`, calendars: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-calendars.html`, - classificationEvaluation: `${ELASTICSEARCH_DOCS}evaluate-dfanalytics.html`, + classificationEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-classification.html#ml-dfanalytics-classification-evaluation`, customRules: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-rules.html`, customUrls: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-url.html`, dataFrameAnalytics: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics.html`, featureImportance: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-feature-importance.html`, outlierDetectionRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-finding-outliers.html#ml-dfanalytics-roc`, - regressionEvaluation: `${ELASTICSEARCH_DOCS}evaluate-dfanalytics.html`, - classificationAucRoc: `${ELASTICSEARCH_DOCS}evaluate-dfanalytics.html`, + regressionEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-regression.html#ml-dfanalytics-regression-evaluation`, + classificationAucRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-classification.html#ml-dfanalytics-class-aucroc`, }, transforms: { guide: `${ELASTICSEARCH_DOCS}transforms.html`, From 07bd76c2a3a85cb4b1d18ae44eda56f04d4a40aa Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 8 Jul 2021 08:58:59 -0700 Subject: [PATCH 07/77] [App Search] Result Settings: Max size input fixes (#104755) * Fix "Max size" number inputs cutting off the "No limit" placeholder text on Chrome/webkit - textOnly={false} is required for the input to expand in width * Fix handleFieldNumberBlur behavior: - should allow setting a number field back to empty/no limits - dev note: '' is the blank/empty state, which gets parsed to NaN. Random invalid strings ALSO get parsed to '', which is NaN * Fix input not clearing invalid non-number strings For some really odd reason, '' and undefined aren't correctly resetting the input value when a string gets entered into the input. I had to add a space for EUI to actually start clearing it Note that this change completely prevents users from writing non-numbers into the input at all, but this is probably fine/valid honestly --- .../result_settings_table/field_number.test.tsx | 6 +++--- .../result_settings_table/field_number.tsx | 11 +++++------ .../result_settings_table/text_fields_body.tsx | 4 ++-- .../result_settings_table/text_fields_header.tsx | 4 ++-- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.test.tsx index 3ac50d906e9c46..c012167f67818a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.test.tsx @@ -54,7 +54,7 @@ describe('FieldNumber', () => { }} /> ); - expect(wrapper.find(EuiFieldNumber).prop('value')).toEqual(''); + expect(wrapper.find(EuiFieldNumber).prop('value')).toEqual(' '); }); it('is disabled if the [fieldEnabledProperty] in fieldSettings is false', () => { @@ -90,10 +90,10 @@ describe('FieldNumber', () => { expect(props.updateAction).toHaveBeenCalledWith('foo', 21); }); - it('will call updateAction on blur using the minimum possible value if the current value is something other than a number', () => { + it('will call clearAction on blur if the current value is something other than a number', () => { const wrapper = shallow(); wrapper.simulate('blur', { target: { value: '' } }); - expect(props.updateAction).toHaveBeenCalledWith('foo', 20); + expect(props.clearAction).toHaveBeenCalledWith('foo'); }); it('will call updateAction on blur using the minimum possible value if the value is something lower than the minimum', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.tsx index cd7bab3c6f594a..f16bab5234ab1f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.tsx @@ -43,11 +43,10 @@ const handleFieldNumberBlur = ( clearAction: (fieldName: string) => void ) => { return (e: FocusEvent) => { - const value = parseInt(e.target.value, 10); - const fieldValue = Math.min( - SIZE_FIELD_MAXIMUM, - Math.max(SIZE_FIELD_MINIMUM, isNaN(value) ? 0 : value) - ); + let fieldValue = parseInt(e.target.value, 10); + if (!isNaN(fieldValue)) { + fieldValue = Math.min(SIZE_FIELD_MAXIMUM, Math.max(SIZE_FIELD_MINIMUM, fieldValue)); + } updateOrClearSizeForField(fieldName, fieldValue, updateAction, clearAction); }; }; @@ -74,7 +73,7 @@ export const FieldNumber: React.FC = ({ value={ typeof fieldSettings[fieldSizeProperty] === 'number' ? (fieldSettings[fieldSizeProperty] as number) - : '' + : ' ' // Without the space, invalid non-numbers don't get cleared for some reason } placeholder={i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.resultSettings.numberFieldPlaceholder', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.tsx index 3a2eb20fecdf00..c3b46f5852724a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.tsx @@ -56,7 +56,7 @@ export const TextFieldsBody: React.FC = () => { }} /> - + { }} /> - + { { defaultMessage: 'Raw' } )} - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.column.maxSizeTitle', { defaultMessage: 'Max size' } @@ -48,7 +48,7 @@ export const TextFieldsHeader: React.FC = () => { { defaultMessage: 'Fallback' } )} - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.column.maxSizeTitle', { defaultMessage: 'Max size' } From bde745539ee028db7bf69cf1f20140752dc81c33 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 8 Jul 2021 18:18:06 +0200 Subject: [PATCH 08/77] [Exploratory view] Fix mobile device distribution (#104626) --- .../configurations/constants/constants.ts | 3 ++- .../configurations/lens_attributes.test.ts | 1 + .../configurations/lens_attributes.ts | 7 +++++-- .../mobile/device_distribution_config.ts | 12 +++++++++--- .../configurations/mobile/distribution_config.ts | 5 +---- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index dd48cf3f7eeb8d..ba1f2214223e3c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -44,6 +44,7 @@ import { TAGS_LABEL, TBT_LABEL, URL_LABEL, + BACKEND_TIME_LABEL, } from './labels'; export const DEFAULT_TIME = { from: 'now-1h', to: 'now' }; @@ -66,7 +67,7 @@ export const FieldLabels: Record = { [TBT_FIELD]: TBT_LABEL, [FID_FIELD]: FID_LABEL, [CLS_FIELD]: CLS_LABEL, - [TRANSACTION_TIME_TO_FIRST_BYTE]: 'Page load time', + [TRANSACTION_TIME_TO_FIRST_BYTE]: BACKEND_TIME_LABEL, 'monitor.id': MONITOR_ID_LABEL, 'monitor.status': MONITOR_STATUS_LABEL, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index 0be64677586c1a..ae70bbdcfa3b8c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -411,6 +411,7 @@ describe('Lens Attribute', () => { sourceField: USER_AGENT_NAME, layerId: 'layer0', indexPattern: mockIndexPattern, + labels: layerConfig.seriesConfig.labels, }); expect(lnsAttr.visualization.layers).toEqual([ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 5734cd15926921..058d3f4751f11f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -30,7 +30,6 @@ import { import { urlFiltersToKueryString } from '../utils/stringify_kueries'; import { ExistsFilter, IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { - FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN, TERMS_COLUMN, @@ -125,17 +124,19 @@ export class LensAttributes { getBreakdownColumn({ sourceField, layerId, + labels, indexPattern, }: { sourceField: string; layerId: string; + labels: Record; indexPattern: IndexPattern; }): TermsIndexPatternColumn { const fieldMeta = indexPattern.getFieldByName(sourceField); return { sourceField, - label: `Top values of ${FieldLabels[sourceField]}`, + label: `Top values of ${labels[sourceField]}`, dataType: fieldMeta?.type as DataType, operationType: 'terms', scale: 'ordinal', @@ -304,6 +305,7 @@ export class LensAttributes { layerId, indexPattern: layerConfig.indexPattern, sourceField: layerConfig.breakdown || layerConfig.seriesConfig.breakdownFields[0], + labels: layerConfig.seriesConfig.labels, }); } @@ -583,6 +585,7 @@ export class LensAttributes { layerId, sourceField: breakdown, indexPattern: layerConfig.indexPattern, + labels: layerConfig.seriesConfig.labels, }), } : {}), diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts index 98979b9922a862..d1612a08f5551e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, USE_BREAK_DOWN_COLUMN } from '../constants'; +import { FieldLabels, REPORT_METRIC_FIELD, USE_BREAK_DOWN_COLUMN } from '../constants'; import { buildPhraseFilter } from '../utils'; import { SERVICE_NAME } from '../constants/elasticsearch_fieldnames'; import { MOBILE_APP, NUMBER_OF_DEVICES } from '../constants/labels'; @@ -22,9 +22,8 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps) }, yAxisColumns: [ { - sourceField: 'labels.device_id', + sourceField: REPORT_METRIC_FIELD, operationType: 'unique_count', - label: NUMBER_OF_DEVICES, }, ], hasOperationType: false, @@ -39,6 +38,13 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps) ...MobileFields, [SERVICE_NAME]: MOBILE_APP, }, + metricOptions: [ + { + id: 'labels.device_id', + field: 'labels.device_id', + label: NUMBER_OF_DEVICES, + }, + ], definitionFields: [SERVICE_NAME], }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts index b9894347d96c07..9b1c4c8da3e9b9 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; +import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -49,19 +49,16 @@ export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): S label: RESPONSE_LATENCY, field: TRANSACTION_DURATION, id: TRANSACTION_DURATION, - columnType: OPERATION_COLUMN, }, { label: MEMORY_USAGE, field: METRIC_SYSTEM_MEMORY_USAGE, id: METRIC_SYSTEM_MEMORY_USAGE, - columnType: OPERATION_COLUMN, }, { label: CPU_USAGE, field: METRIC_SYSTEM_CPU_USAGE, id: METRIC_SYSTEM_CPU_USAGE, - columnType: OPERATION_COLUMN, }, ], }; From 04c05f807727f401a187b13e473597e273d96397 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 8 Jul 2021 18:19:09 +0200 Subject: [PATCH 09/77] [Exploratory view] Fix core web vital breakdown (#104630) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../configurations/lens_attributes.ts | 20 ++- .../rum/core_web_vitals_config.test.ts | 39 +++++ .../rum/core_web_vitals_config.ts | 7 +- .../test_data/sample_attribute_cwv.ts | 149 ++++++++++++++++++ .../columns/report_types_col.test.tsx | 1 - .../columns/report_types_col.tsx | 1 + .../utils/observability_index_patterns.ts | 13 +- 7 files changed, 216 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 058d3f4751f11f..dfb17ee470d355 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -346,7 +346,7 @@ export class LensAttributes { if (fieldName === RECORDS_FIELD || columnType === FILTER_RECORDS) { return this.getRecordsColumn( - columnLabel || label, + label || columnLabel, colIndex !== undefined ? columnFilters?.[colIndex] : undefined, timeScale ); @@ -435,6 +435,8 @@ export class LensAttributes { if (yAxisColumns.length === 1) { return lensColumns; } + + // starting from 1 index since 0 column is used as main column for (let i = 1; i < yAxisColumns.length; i++) { const { sourceField, operationType, label } = yAxisColumns[i]; @@ -557,16 +559,21 @@ export class LensAttributes { const layerConfigs = this.layerConfigs; layerConfigs.forEach((layerConfig, index) => { - const { breakdown } = layerConfig; + const { breakdown, seriesConfig } = layerConfig; const layerId = `layer${index}`; const columnFilter = this.getLayerFilters(layerConfig, layerConfigs.length); const timeShift = this.getTimeShift(this.layerConfigs[0], layerConfig, index); const mainYAxis = this.getMainYAxis(layerConfig, layerId, columnFilter); + + const { sourceField } = seriesConfig.xAxisColumn; + layers[layerId] = { columnOrder: [ `x-axis-column-${layerId}`, - ...(breakdown ? [`breakdown-column-${layerId}`] : []), + ...(breakdown && sourceField !== USE_BREAK_DOWN_COLUMN + ? [`breakdown-column-${layerId}`] + : []), `y-axis-column-${layerId}`, ...Object.keys(this.getChildYAxises(layerConfig, layerId, columnFilter)), ], @@ -578,7 +585,7 @@ export class LensAttributes { filter: { query: columnFilter, language: 'kuery' }, ...(timeShift ? { timeShift } : {}), }, - ...(breakdown && breakdown !== USE_BREAK_DOWN_COLUMN + ...(breakdown && sourceField !== USE_BREAK_DOWN_COLUMN ? // do nothing since this will be used a x axis source { [`breakdown-column-${layerId}`]: this.getBreakdownColumn({ @@ -620,7 +627,10 @@ export class LensAttributes { { forAccessor: `y-axis-column-layer${index}` }, ], xAccessor: `x-axis-column-layer${index}`, - ...(layerConfig.breakdown ? { splitAccessor: `breakdown-column-layer${index}` } : {}), + ...(layerConfig.breakdown && + layerConfig.seriesConfig.xAxisColumn.sourceField !== USE_BREAK_DOWN_COLUMN + ? { splitAccessor: `breakdown-column-layer${index}` } + : {}), })), ...(this.layerConfigs[0].seriesConfig.yTitle ? { yTitle: this.layerConfigs[0].seriesConfig.yTitle } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts new file mode 100644 index 00000000000000..07bb13f957e456 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockAppIndexPattern, mockIndexPattern } from '../../rtl_helpers'; +import { getDefaultConfigs } from '../default_configs'; +import { LayerConfig, LensAttributes } from '../lens_attributes'; +import { sampleAttributeCoreWebVital } from '../test_data/sample_attribute_cwv'; +import { SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames'; + +describe('Core web vital config test', function () { + mockAppIndexPattern(); + + const seriesConfig = getDefaultConfigs({ + reportType: 'core-web-vitals', + dataType: 'ux', + indexPattern: mockIndexPattern, + }); + + let lnsAttr: LensAttributes; + + const layerConfig: LayerConfig = { + seriesConfig, + indexPattern: mockIndexPattern, + reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, + time: { from: 'now-15m', to: 'now' }, + breakdown: USER_AGENT_OS, + }; + + beforeEach(() => { + lnsAttr = new LensAttributes([layerConfig]); + }); + it('should return expected json', function () { + expect(lnsAttr.getJSON()).toEqual(sampleAttributeCoreWebVital); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts index 1d04a9b3895035..62455df2480855 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts @@ -31,6 +31,7 @@ import { URL_FULL, SERVICE_ENVIRONMENT, } from '../constants/elasticsearch_fieldnames'; +import { CLS_LABEL, FID_LABEL, LCP_LABEL } from '../constants/labels'; export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesConfig { const statusPallete = euiPaletteForStatus(3); @@ -91,7 +92,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon metricOptions: [ { id: LCP_FIELD, - label: 'Largest contentful paint', + label: LCP_LABEL, columnType: FILTER_RECORDS, columnFilters: [ { @@ -109,7 +110,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon ], }, { - label: 'First input delay', + label: FID_LABEL, id: FID_FIELD, columnType: FILTER_RECORDS, columnFilters: [ @@ -128,7 +129,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon ], }, { - label: 'Cumulative layout shift', + label: CLS_LABEL, id: CLS_FIELD, columnType: FILTER_RECORDS, columnFilters: [ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts new file mode 100644 index 00000000000000..2087b85b818866 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export const sampleAttributeCoreWebVital = { + description: '', + references: [ + { + id: 'apm-*', + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: 'apm-*', + name: 'indexpattern-datasource-layer-layer0', + type: 'index-pattern', + }, + ], + state: { + datasourceStates: { + indexpattern: { + layers: { + layer0: { + columnOrder: [ + 'x-axis-column-layer0', + 'y-axis-column-layer0', + 'y-axis-column-1', + 'y-axis-column-2', + ], + columns: { + 'x-axis-column-layer0': { + dataType: 'string', + isBucketed: true, + label: 'Top values of Operating system', + operationType: 'terms', + params: { + missingBucket: false, + orderBy: { + columnId: 'y-axis-column-layer0', + type: 'column', + }, + orderDirection: 'desc', + otherBucket: true, + size: 10, + }, + scale: 'ordinal', + sourceField: 'user_agent.os.name', + }, + 'y-axis-column-1': { + dataType: 'number', + filter: { + language: 'kuery', + query: + 'transaction.marks.agent.largestContentfulPaint > 2500 and transaction.marks.agent.largestContentfulPaint < 4000', + }, + isBucketed: false, + label: 'Average', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + 'y-axis-column-2': { + dataType: 'number', + filter: { + language: 'kuery', + query: 'transaction.marks.agent.largestContentfulPaint > 4000', + }, + isBucketed: false, + label: 'Poor', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + 'y-axis-column-layer0': { + dataType: 'number', + filter: { + language: 'kuery', + query: 'transaction.type: page-load and processor.event: transaction', + }, + isBucketed: false, + label: 'Good', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + axisTitlesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + curveType: 'CURVE_MONOTONE_X', + fittingFunction: 'Linear', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + layers: [ + { + accessors: ['y-axis-column-layer0', 'y-axis-column-1', 'y-axis-column-2'], + layerId: 'layer0', + seriesType: 'bar_horizontal_percentage_stacked', + xAccessor: 'x-axis-column-layer0', + yConfig: [ + { + color: '#209280', + forAccessor: 'y-axis-column', + }, + { + color: '#d6bf57', + forAccessor: 'y-axis-column-1', + }, + { + color: '#cc5642', + forAccessor: 'y-axis-column-2', + }, + ], + }, + ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'line', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'hide', + }, + }, + title: 'Prefilled from exploratory view app', + visualizationType: 'lnsXY', +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx index 07048d47b2bc32..12ae8560453c97 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx @@ -36,7 +36,6 @@ describe('ReportTypesCol', function () { fireEvent.click(screen.getByText(/KPI over time/i)); expect(setSeries).toHaveBeenCalledWith(seriesId, { - breakdown: 'user_agent.name', dataType: 'ux', selectedMetricField: undefined, reportType: 'kpi-over-time', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx index 396f8c4f1deb39..c4eebbfaca3ebf 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -78,6 +78,7 @@ export function ReportTypesCol({ seriesId, reportTypes }: Props) { ...restSeries, reportType, selectedMetricField: undefined, + breakdown: undefined, time: restSeries?.time ?? DEFAULT_TIME, }); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts index 634408dd614da7..964de86ddf377e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts @@ -100,11 +100,14 @@ export class ObservabilityIndexPatterns { if (defaultFieldFormats && defaultFieldFormats.length > 0) { let isParamsDifferent = false; defaultFieldFormats.forEach(({ field, format }) => { - const fieldFormat = indexPattern.getFormatterForField(indexPattern.getFieldByName(field)!); - const params = fieldFormat.params(); - if (!isParamsSame(params, format.params)) { - indexPattern.setFieldFormat(field, format); - isParamsDifferent = true; + const fieldByName = indexPattern.getFieldByName(field); + if (fieldByName) { + const fieldFormat = indexPattern.getFormatterForField(fieldByName); + const params = fieldFormat.params(); + if (!isParamsSame(params, format.params)) { + indexPattern.setFieldFormat(field, format); + isParamsDifferent = true; + } } }); if (isParamsDifferent) { From 035ff66717449582be87aeb26fcc72de928ceb9e Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 8 Jul 2021 11:22:07 -0500 Subject: [PATCH 10/77] [ML] Fix Index Data Visualizer error if index pattern has histogram field type (#104553) * [ML] Add NON_AGGREGATABLE_FIELD_TYPES, add icon type * Fix types * Add histogram icon, fix types showing for hidden fields Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../data_visualizer/common/constants.ts | 21 +++++++++++++++ .../field_type_icon/field_type_icon.tsx | 3 +++ .../field_types_filter/field_types_filter.tsx | 14 +--------- .../common/util/field_types_utils.ts | 3 +++ .../index_data_visualizer_view.tsx | 27 ++++++++++--------- .../search_panel/field_type_filter.tsx | 16 ++--------- .../data_loader/data_loader.ts | 5 ++-- 7 files changed, 47 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/data_visualizer/common/constants.ts b/x-pack/plugins/data_visualizer/common/constants.ts index 7e0fe65632ae33..55ebdf9a196d65 100644 --- a/x-pack/plugins/data_visualizer/common/constants.ts +++ b/x-pack/plugins/data_visualizer/common/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { KBN_FIELD_TYPES } from '../../../../src/plugins/data/common'; + export const UI_SETTING_MAX_FILE_SIZE = 'fileUpload:maxFileSize'; export const MB = Math.pow(2, 20); @@ -27,7 +29,26 @@ export const JOB_FIELD_TYPES = { KEYWORD: 'keyword', NUMBER: 'number', TEXT: 'text', + HISTOGRAM: 'histogram', UNKNOWN: 'unknown', } as const; +export const JOB_FIELD_TYPES_OPTIONS = { + [JOB_FIELD_TYPES.BOOLEAN]: { name: 'Boolean', icon: 'tokenBoolean' }, + [JOB_FIELD_TYPES.DATE]: { name: 'Date', icon: 'tokenDate' }, + [JOB_FIELD_TYPES.GEO_POINT]: { name: 'Geo point', icon: 'tokenGeo' }, + [JOB_FIELD_TYPES.GEO_SHAPE]: { name: 'Geo shape', icon: 'tokenGeo' }, + [JOB_FIELD_TYPES.IP]: { name: 'IP address', icon: 'tokenIP' }, + [JOB_FIELD_TYPES.KEYWORD]: { name: 'Keyword', icon: 'tokenKeyword' }, + [JOB_FIELD_TYPES.NUMBER]: { name: 'Number', icon: 'tokenNumber' }, + [JOB_FIELD_TYPES.TEXT]: { name: 'Text', icon: 'tokenString' }, + [JOB_FIELD_TYPES.HISTOGRAM]: { name: 'Histogram', icon: 'tokenNumber' }, + [JOB_FIELD_TYPES.UNKNOWN]: { name: 'Unknown' }, +}; + export const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score']; + +export const NON_AGGREGATABLE_FIELD_TYPES = new Set([ + KBN_FIELD_TYPES.GEO_SHAPE, + KBN_FIELD_TYPES.HISTOGRAM, +]); diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx index 50823006db3b62..ee4b4f8171d7d8 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx @@ -72,6 +72,9 @@ export const FieldTypeIcon: FC = ({ iconType = 'tokenNumber'; color = fieldName !== undefined ? 'euiColorVis1' : 'euiColorVis2'; break; + case JOB_FIELD_TYPES.HISTOGRAM: + iconType = 'tokenHistogram'; + color = 'euiColorVis7'; case JOB_FIELD_TYPES.UNKNOWN: // Use defaults break; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx index 152926ad84ba71..511a068f305f93 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx @@ -14,19 +14,7 @@ import type { FileBasedUnknownFieldVisConfig, } from '../stats_table/types/field_vis_config'; import { FieldTypeIcon } from '../field_type_icon'; -import { JOB_FIELD_TYPES } from '../../../../../common'; - -const JOB_FIELD_TYPES_OPTIONS = { - [JOB_FIELD_TYPES.BOOLEAN]: { name: 'Boolean', icon: 'tokenBoolean' }, - [JOB_FIELD_TYPES.DATE]: { name: 'Date', icon: 'tokenDate' }, - [JOB_FIELD_TYPES.GEO_POINT]: { name: 'Geo point', icon: 'tokenGeo' }, - [JOB_FIELD_TYPES.GEO_SHAPE]: { name: 'Geo shape', icon: 'tokenGeo' }, - [JOB_FIELD_TYPES.IP]: { name: 'IP address', icon: 'tokenIP' }, - [JOB_FIELD_TYPES.KEYWORD]: { name: 'Keyword', icon: 'tokenKeyword' }, - [JOB_FIELD_TYPES.NUMBER]: { name: 'Number', icon: 'tokenNumber' }, - [JOB_FIELD_TYPES.TEXT]: { name: 'Text', icon: 'tokenString' }, - [JOB_FIELD_TYPES.UNKNOWN]: { name: 'Unknown' }, -}; +import { JOB_FIELD_TYPES_OPTIONS } from '../../../../../common'; interface Props { fields: Array; diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts index e0a6c8ebf85c9a..98d43059e5ee38 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts @@ -78,6 +78,9 @@ export function kbnTypeToJobType(field: IndexPatternField) { case KBN_FIELD_TYPES.GEO_SHAPE: type = JOB_FIELD_TYPES.GEO_SHAPE; break; + case KBN_FIELD_TYPES.HISTOGRAM: + type = JOB_FIELD_TYPES.HISTOGRAM; + break; default: break; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx index c9ae3cf7f69a73..ec55eddec2a795 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx @@ -56,7 +56,7 @@ import { useDataVisualizerKibana } from '../../../kibana_context'; import { FieldCountPanel } from '../../../common/components/field_count_panel'; import { DocumentCountContent } from '../../../common/components/document_count_content'; import { DataLoader } from '../../data_loader/data_loader'; -import { JOB_FIELD_TYPES } from '../../../../../common'; +import { JOB_FIELD_TYPES, OMIT_FIELDS } from '../../../../../common'; import { useTimefilter } from '../../hooks/use_time_filter'; import { kbnTypeToJobType } from '../../../common/util/field_types_utils'; import { SearchPanel } from '../search_panel'; @@ -204,18 +204,21 @@ export const IndexDataVisualizerView: FC = (dataVi } }, [currentIndexPattern, toasts]); - // Obtain the list of non metric field types which appear in the index pattern. - let indexedFieldTypes: JobFieldType[] = []; const indexPatternFields: IndexPatternField[] = currentIndexPattern.fields; - indexPatternFields.forEach((field) => { - if (field.scripted !== true) { - const dataVisualizerType: JobFieldType | undefined = kbnTypeToJobType(field); - if (dataVisualizerType !== undefined && !indexedFieldTypes.includes(dataVisualizerType)) { - indexedFieldTypes.push(dataVisualizerType); + + const fieldTypes = useMemo(() => { + // Obtain the list of non metric field types which appear in the index pattern. + const indexedFieldTypes: JobFieldType[] = []; + indexPatternFields.forEach((field) => { + if (!OMIT_FIELDS.includes(field.name) && field.scripted !== true) { + const dataVisualizerType: JobFieldType | undefined = kbnTypeToJobType(field); + if (dataVisualizerType !== undefined && !indexedFieldTypes.includes(dataVisualizerType)) { + indexedFieldTypes.push(dataVisualizerType); + } } - } - }); - indexedFieldTypes = indexedFieldTypes.sort(); + }); + return indexedFieldTypes.sort(); + }, [indexPatternFields]); const defaults = getDefaultPageState(); @@ -859,7 +862,7 @@ export const IndexDataVisualizerView: FC = (dataVi samplerShardSize={samplerShardSize} setSamplerShardSize={setSamplerShardSize} overallStats={overallStats} - indexedFieldTypes={indexedFieldTypes} + indexedFieldTypes={fieldTypes} setVisibleFieldTypes={setVisibleFieldTypes} visibleFieldTypes={visibleFieldTypes} visibleFieldNames={visibleFieldNames} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx index 4f9de09dc670e4..a4286bc4e09d16 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx @@ -8,22 +8,10 @@ import React, { FC, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { JOB_FIELD_TYPES, JobFieldType } from '../../../../../common'; +import { JOB_FIELD_TYPES_OPTIONS, JobFieldType } from '../../../../../common'; import { FieldTypeIcon } from '../../../common/components/field_type_icon'; import { MultiSelectPicker, Option } from '../../../common/components/multi_select_picker'; -const ML_JOB_FIELD_TYPES_OPTIONS = { - [JOB_FIELD_TYPES.BOOLEAN]: { name: 'Boolean', icon: 'tokenBoolean' }, - [JOB_FIELD_TYPES.DATE]: { name: 'Date', icon: 'tokenDate' }, - [JOB_FIELD_TYPES.GEO_POINT]: { name: 'Geo point', icon: 'tokenGeo' }, - [JOB_FIELD_TYPES.GEO_SHAPE]: { name: 'Geo shape', icon: 'tokenGeo' }, - [JOB_FIELD_TYPES.IP]: { name: 'IP address', icon: 'tokenIP' }, - [JOB_FIELD_TYPES.KEYWORD]: { name: 'Keyword', icon: 'tokenKeyword' }, - [JOB_FIELD_TYPES.NUMBER]: { name: 'Number', icon: 'tokenNumber' }, - [JOB_FIELD_TYPES.TEXT]: { name: 'Text', icon: 'tokenString' }, - [JOB_FIELD_TYPES.UNKNOWN]: { name: 'Unknown' }, -}; - export const DatavisualizerFieldTypeFilter: FC<{ indexedFieldTypes: JobFieldType[]; setVisibleFieldTypes(q: string[]): void; @@ -31,7 +19,7 @@ export const DatavisualizerFieldTypeFilter: FC<{ }> = ({ indexedFieldTypes, setVisibleFieldTypes, visibleFieldTypes }) => { const options: Option[] = useMemo(() => { return indexedFieldTypes.map((indexedFieldName) => { - const item = ML_JOB_FIELD_TYPES_OPTIONS[indexedFieldName]; + const item = JOB_FIELD_TYPES_OPTIONS[indexedFieldName]; return { value: indexedFieldName, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts index 468bd3a2bd7ee1..4a3c971cc57cd1 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts @@ -10,8 +10,7 @@ import { CoreSetup } from 'kibana/public'; import { estypes } from '@elastic/elasticsearch'; import { i18n } from '@kbn/i18n'; import { IndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; -import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; -import { OMIT_FIELDS } from '../../../../common/constants'; +import { NON_AGGREGATABLE_FIELD_TYPES, OMIT_FIELDS } from '../../../../common/constants'; import { FieldRequestConfig } from '../../../../common/types'; import { getVisualizerFieldStats, getVisualizerOverallStats } from '../services/visualizer_stats'; @@ -49,7 +48,7 @@ export class DataLoader { this._indexPattern.fields.forEach((field) => { const fieldName = field.displayName !== undefined ? field.displayName : field.name; if (this.isDisplayField(fieldName) === true) { - if (field.aggregatable === true && field.type !== KBN_FIELD_TYPES.GEO_SHAPE) { + if (field.aggregatable === true && !NON_AGGREGATABLE_FIELD_TYPES.has(field.type)) { aggregatableFields.push(field.name); } else { nonAggregatableFields.push(field.name); From d4ecc018b7c6ef3ced47e19cf4d4c2665741da31 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 8 Jul 2021 17:22:41 +0100 Subject: [PATCH 11/77] [ML] Fix error handling in job search bar (#104759) --- .../jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx index d5a62a91d3bd54..8736e27619ee54 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx @@ -63,7 +63,7 @@ export const JobFilterBar: FC = ({ queryText, setFilters }) = }, [queryText]); const onChange: EuiSearchBarProps['onChange'] = ({ query, error: queryError }) => { - if (error) { + if (queryError) { setError(queryError); } else { setFilters(query); From 15f0f7f72763952c690161915fa742b18b7551a2 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Thu, 8 Jul 2021 12:32:12 -0400 Subject: [PATCH 12/77] [Uptime] [Alerting] fix tls legacy descriptions (#104873) --- .../public/lib/alert_types/translations.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts b/x-pack/plugins/uptime/public/lib/alert_types/translations.ts index bb4af761d240d0..5122120479cf77 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/translations.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; export const TlsTranslations = { - defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.legacy.defaultActionMessage', { + defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', { defaultMessage: `Detected TLS certificate {commonName} from issuer {issuer} is {status}. Certificate {summary} `, values: { @@ -18,17 +18,16 @@ export const TlsTranslations = { status: '{{state.status}}', }, }), - name: i18n.translate('xpack.uptime.alerts.tls.legacy.clientName', { - defaultMessage: 'Uptime TLS (Legacy)', + name: i18n.translate('xpack.uptime.alerts.tls.clientName', { + defaultMessage: 'Uptime TLS', }), - description: i18n.translate('xpack.uptime.alerts.tls.legacy.description', { - defaultMessage: - 'Alert when the TLS certificate of an Uptime monitor is about to expire. This alert will be deprecated in a future version.', + description: i18n.translate('xpack.uptime.alerts.tls.description', { + defaultMessage: 'Alert when the TLS certificate of an Uptime monitor is about to expire.', }), }; export const TlsTranslationsLegacy = { - defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', { + defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.legacy.defaultActionMessage', { defaultMessage: `Detected {count} TLS certificates expiring or becoming too old. {expiringConditionalOpen} Expiring cert count: {expiringCount} @@ -51,11 +50,12 @@ Aging Certificates: {agingCommonNameAndDate} agingConditionalClose: '{{/state.hasAging}}', }, }), - name: i18n.translate('xpack.uptime.alerts.tls.clientName', { + name: i18n.translate('xpack.uptime.alerts.tls.legacy.clientName', { defaultMessage: 'Uptime TLS', }), - description: i18n.translate('xpack.uptime.alerts.tls.description', { - defaultMessage: 'Alert when the TLS certificate of an Uptime monitor is about to expire.', + description: i18n.translate('xpack.uptime.alerts.tls.legacy.description', { + defaultMessage: + 'Alert when the TLS certificate of an Uptime monitor is about to expire. This rule type will be deprecated in a future version.', }), }; From a7fe96a428c9821c39bb1dd59676ed322429b31c Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Thu, 8 Jul 2021 12:59:55 -0400 Subject: [PATCH 13/77] Fix background color on view assets button (#104887) Fixes #104813 --- .../epm/screens/detail/policies/package_policies.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index 96e4071e9b4640..63372e435cfa1c 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -151,7 +151,10 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps /> - + {i18n.translate('xpack.fleet.epm.agentEnrollment.viewDataAssetsLabel', { defaultMessage: 'View assets', })} From fa8dd5603eb8e99395945d789f68f2db3ce77d5f Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Thu, 8 Jul 2021 12:28:00 -0500 Subject: [PATCH 14/77] Use deployment id instead of cloud id in tutorial link (#104779) Fixes #104611. --- x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts | 7 ++++--- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts b/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts index 55adc756f31af3..a595ae1dc8a8b6 100644 --- a/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts +++ b/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts @@ -46,7 +46,8 @@ export function createElasticCloudInstructions( function getApmServerInstructionSet( cloudSetup?: CloudSetup ): InstructionSetSchema { - const cloudId = cloudSetup?.cloudId; + const deploymentId = cloudSetup?.deploymentId; + return { title: i18n.translate('xpack.apm.tutorial.apmServer.title', { defaultMessage: 'APM Server', @@ -59,8 +60,8 @@ function getApmServerInstructionSet( title: 'Enable the APM Server in the ESS console', textPre: i18n.translate('xpack.apm.tutorial.elasticCloud.textPre', { defaultMessage: - 'To enable the APM Server go to [the Elastic Cloud console](https://cloud.elastic.co/deployments?q={cloudId}) and enable APM in the deployment settings. Once enabled, refresh this page.', - values: { cloudId }, + 'To enable the APM Server go to [the Elastic Cloud console](https://cloud.elastic.co/deployments/{deploymentId}/edit) and enable APM in the deployment settings. Once enabled, refresh this page.', + values: { deploymentId }, }), }, ], diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fd0060f83a3e48..b21d4afbe00205 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5937,7 +5937,6 @@ "xpack.apm.transactionsTable.notFoundLabel": "トランザクションが見つかりませんでした。", "xpack.apm.transactionsTable.throughputColumnLabel": "スループット", "xpack.apm.tutorial.apmServer.title": "APM Server", - "xpack.apm.tutorial.elasticCloud.textPre": "APM Server を有効にするには、[the Elastic Cloud console] (https://cloud.elastic.co/deployments?q={cloudId}) に移動し、展開設定で APM を有効にします。有効になったら、このページを更新してください。", "xpack.apm.tutorial.elasticCloudInstructions.title": "APM エージェント", "xpack.apm.tutorial.specProvider.artifacts.application.label": "APM を起動", "xpack.apm.unitLabel": "単位を選択", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d2a2f0be215d37..27a5a06f9ca7c8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5975,7 +5975,6 @@ "xpack.apm.transactionsTable.notFoundLabel": "未找到任何事务。", "xpack.apm.transactionsTable.throughputColumnLabel": "吞吐量", "xpack.apm.tutorial.apmServer.title": "APM Server", - "xpack.apm.tutorial.elasticCloud.textPre": "要启用 APM Server,请前往 [Elastic Cloud 控制台](https://cloud.elastic.co/deployments?q={cloudId}) 并在部署设置中启用 APM。启用后,请刷新此页面。", "xpack.apm.tutorial.elasticCloudInstructions.title": "APM 代理", "xpack.apm.tutorial.specProvider.artifacts.application.label": "启动 APM", "xpack.apm.unitLabel": "选择单位", From 11b7e33a0ea94dacec9420a7fac66e7af4b2d53c Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Thu, 8 Jul 2021 13:35:00 -0400 Subject: [PATCH 15/77] [canvas] Reduce bundle by registering Canvas Plugin API on mount (#104264) --- .../public/render_expressions.tsx | 4 +++- .../public/run_expressions.tsx | 2 +- .../expressions_explorer/expressions.ts | 2 +- x-pack/plugins/canvas/public/plugin.tsx | 21 ++++++++++--------- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/examples/expressions_explorer/public/render_expressions.tsx b/examples/expressions_explorer/public/render_expressions.tsx index 20a8b56edb3f8d..d91db964c5352a 100644 --- a/examples/expressions_explorer/public/render_expressions.tsx +++ b/examples/expressions_explorer/public/render_expressions.tsx @@ -34,7 +34,9 @@ interface Props { } export function RenderExpressionsExample({ expressions, inspector }: Props) { - const [expression, updateExpression] = useState('markdown "## expressions explorer rendering"'); + const [expression, updateExpression] = useState( + 'markdownVis "## expressions explorer rendering"' + ); const expressionChanged = (value: string) => { updateExpression(value); diff --git a/examples/expressions_explorer/public/run_expressions.tsx b/examples/expressions_explorer/public/run_expressions.tsx index a635fab7ec8ae9..93cab0e9f2b6fa 100644 --- a/examples/expressions_explorer/public/run_expressions.tsx +++ b/examples/expressions_explorer/public/run_expressions.tsx @@ -35,7 +35,7 @@ interface Props { } export function RunExpressionsExample({ expressions, inspector }: Props) { - const [expression, updateExpression] = useState('markdown "## expressions explorer"'); + const [expression, updateExpression] = useState('markdownVis "## expressions explorer"'); const [result, updateResult] = useState({}); const expressionChanged = (value: string) => { diff --git a/test/examples/expressions_explorer/expressions.ts b/test/examples/expressions_explorer/expressions.ts index 4c240653b5fdd0..9aef64a392a7b2 100644 --- a/test/examples/expressions_explorer/expressions.ts +++ b/test/examples/expressions_explorer/expressions.ts @@ -22,7 +22,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { await retry.try(async () => { const text = await testSubjects.getVisibleText('expressionResult'); expect(text).to.be( - '{\n "type": "render",\n "as": "markdown",\n "value": {\n "content": "## expressions explorer",\n "font": {\n "type": "style",\n "spec": {\n "fontFamily": "\'Open Sans\', Helvetica, Arial, sans-serif",\n "fontWeight": "normal",\n "fontStyle": "normal",\n "textDecoration": "none",\n "textAlign": "left",\n "fontSize": "14px",\n "lineHeight": "1"\n },\n "css": "font-family:\'Open Sans\', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:left;font-size:14px;line-height:1"\n },\n "openLinksInNewTab": false\n }\n}' + '{\n "type": "render",\n "as": "markdown_vis",\n "value": {\n "visType": "markdown",\n "visParams": {\n "markdown": "## expressions explorer",\n "openLinksInNewTab": false,\n "fontSize": 12\n }\n }\n}' ); }); }); diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 543c159bae145c..101f64e53b685d 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -30,8 +30,6 @@ import { Start as InspectorStart } from '../../../../src/plugins/inspector/publi import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public'; import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { getPluginApi, CanvasApi } from './plugin_api'; -import { CanvasSrcPlugin } from '../canvas_plugin_src/plugin'; -import { pluginServices } from './services'; import { pluginServiceRegistry } from './services/kibana'; export { CoreStart, CoreSetup }; @@ -75,14 +73,10 @@ export type CanvasStart = void; export class CanvasPlugin implements Plugin { private appUpdater = new BehaviorSubject(() => ({})); - // TODO: Do we want to completely move canvas_plugin_src into it's own plugin? - private srcPlugin = new CanvasSrcPlugin(); public setup(coreSetup: CoreSetup, setupPlugins: CanvasSetupDeps) { const { api: canvasApi, registries } = getPluginApi(setupPlugins.expressions); - this.srcPlugin.setup(coreSetup, { canvas: canvasApi }); - // Set the nav link to the last saved url if we have one in storage const lastPath = getSessionStorage().get( `${SESSIONSTORAGE_LASTPATH}:${coreSetup.http.basePath.get()}` @@ -101,12 +95,21 @@ export class CanvasPlugin order: 3000, updater$: this.appUpdater, mount: async (params: AppMountParameters) => { - // Load application bundle - const { renderApp, initializeCanvas, teardownCanvas } = await import('./application'); + const { CanvasSrcPlugin } = await import('../canvas_plugin_src/plugin'); + const srcPlugin = new CanvasSrcPlugin(); + srcPlugin.setup(coreSetup, { canvas: canvasApi }); // Get start services const [coreStart, startPlugins] = await coreSetup.getStartServices(); + srcPlugin.start(coreStart, startPlugins); + + const { pluginServices } = await import('./services'); + pluginServices.setRegistry(pluginServiceRegistry.start({ coreStart, startPlugins })); + + // Load application bundle + const { renderApp, initializeCanvas, teardownCanvas } = await import('./application'); + const canvasStore = await initializeCanvas( coreSetup, coreStart, @@ -145,8 +148,6 @@ export class CanvasPlugin } public start(coreStart: CoreStart, startPlugins: CanvasStartDeps) { - this.srcPlugin.start(coreStart, startPlugins); - pluginServices.setRegistry(pluginServiceRegistry.start({ coreStart, startPlugins })); initLoadingIndicator(coreStart.http.addLoadingCountSource); } } From cfe1fe72c5b37e168cd1e7a77a16a3cbfe9bcac6 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Thu, 8 Jul 2021 13:39:00 -0400 Subject: [PATCH 16/77] [canvas] Reduce bundle size by removing dependency on URL library (#104233) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../functions/browser/urlparam.ts | 6 +- .../plugins/canvas/public/lib/modify_url.ts | 90 ------------------- .../test/functional/apps/canvas/smoke_test.js | 3 +- 3 files changed, 4 insertions(+), 95 deletions(-) delete mode 100644 x-pack/plugins/canvas/public/lib/modify_url.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/urlparam.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/urlparam.ts index c8ce7717cd2d2a..9d56278e2c324f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/urlparam.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/urlparam.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { parse } from 'url'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; @@ -43,8 +42,9 @@ export function urlparam(): ExpressionFunctionDefinition< }, }, fn: (input, args) => { - const query = parse(window.location.href, true).query; - return query[args.param] || args.default; + const url = new URL(window.location.href); + const query = url.searchParams; + return query.get(args.param) || args.default; }, }; } diff --git a/x-pack/plugins/canvas/public/lib/modify_url.ts b/x-pack/plugins/canvas/public/lib/modify_url.ts deleted file mode 100644 index 3a275853116d85..00000000000000 --- a/x-pack/plugins/canvas/public/lib/modify_url.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ParsedQuery } from 'query-string'; -import { format as formatUrl, parse as parseUrl, UrlObject } from 'url'; - -/** - * We define our own typings because the current version of @types/node - * declares properties to be optional "hostname?: string". - * Although, parse call returns "hostname: null | string". - */ -export interface URLMeaningfulParts { - auth?: string | null; - hash?: string | null; - hostname?: string | null; - pathname?: string | null; - protocol?: string | null; - slashes?: boolean | null; - port?: string | null; - query: ParsedQuery; -} - -/** - * Takes a URL and a function that takes the meaningful parts - * of the URL as a key-value object, modifies some or all of - * the parts, and returns the modified parts formatted again - * as a url. - * - * Url Parts sent: - * - protocol - * - slashes (does the url have the //) - * - auth - * - hostname (just the name of the host, no port or auth information) - * - port - * - pathname (the path after the hostname, no query or hash, starts - * with a slash if there was a path) - * - query (always an object, even when no query on original url) - * - hash - * - * Why? - * - The default url library in node produces several conflicting - * properties on the "parsed" output. Modifying any of these might - * lead to the modifications being ignored (depending on which - * property was modified) - * - It's not always clear whether to use path/pathname, host/hostname, - * so this tries to add helpful constraints - * - * @param url The string url to parse. - * @param urlModifier A function that will modify the parsed url, or return a new one. - * @returns The modified and reformatted url - */ -export function modifyUrl( - url: string, - urlModifier: (urlParts: URLMeaningfulParts) => Partial | void -) { - const parsed = parseUrl(url, true) as URLMeaningfulParts; - - // Copy over the most specific version of each property. By default, the parsed url includes several - // conflicting properties (like path and pathname + search, or search and query) and keeping track - // of which property is actually used when they are formatted is harder than necessary. - const meaningfulParts: URLMeaningfulParts = { - auth: parsed.auth, - hash: parsed.hash, - hostname: parsed.hostname, - pathname: parsed.pathname, - port: parsed.port, - protocol: parsed.protocol, - query: parsed.query || {}, - slashes: parsed.slashes, - }; - - // The urlModifier modifies the meaningfulParts object, or returns a new one. - const modifiedParts = urlModifier(meaningfulParts) || meaningfulParts; - - // Format the modified/replaced meaningfulParts back into a url. - return formatUrl({ - auth: modifiedParts.auth, - hash: modifiedParts.hash, - hostname: modifiedParts.hostname, - pathname: modifiedParts.pathname, - port: modifiedParts.port, - protocol: modifiedParts.protocol, - query: modifiedParts.query, - slashes: modifiedParts.slashes, - } as UrlObject); -} diff --git a/x-pack/test/functional/apps/canvas/smoke_test.js b/x-pack/test/functional/apps/canvas/smoke_test.js index fcc04aafdbcd85..cb29840c4b2aaf 100644 --- a/x-pack/test/functional/apps/canvas/smoke_test.js +++ b/x-pack/test/functional/apps/canvas/smoke_test.js @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import { parse } from 'url'; export default function canvasSmokeTest({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); @@ -45,7 +44,7 @@ export default function canvasSmokeTest({ getService, getPageObjects }) { const url = await browser.getCurrentUrl(); // remove all the search params, just compare the route - const hashRoute = parse(url).hash.split('?')[0]; + const hashRoute = new URL(url).hash.split('?')[0]; expect(hashRoute).to.equal(`#/workpad/${testWorkpadId}/page/1`); }); }); From 83856767aff6b6d42f86e34e49d2b385468383e8 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 8 Jul 2021 21:00:25 +0300 Subject: [PATCH 17/77] [Cases] Reset selected cases when changing filters (#104839) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/all_cases/all_cases_generic.tsx | 10 +++++++--- .../cases/public/components/all_cases/table.tsx | 7 +++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx index a6d8afc3b8b230..477ea27be99bff 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx @@ -6,8 +6,7 @@ */ import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { EuiProgress } from '@elastic/eui'; -import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; +import { EuiProgress, EuiBasicTable, EuiTableSelectionType } from '@elastic/eui'; import { difference, head, isEmpty, memoize } from 'lodash/fp'; import styled, { css } from 'styled-components'; import classnames from 'classnames'; @@ -113,6 +112,7 @@ export const AllCasesGeneric = React.memo( ); const filterRefetch = useRef<() => void>(); + const tableRef = useRef(); const setFilterRefetch = useCallback( (refetchFilter: () => void) => { filterRefetch.current = refetchFilter; @@ -182,10 +182,13 @@ export const AllCasesGeneric = React.memo( ) { setQueryParams({ sortField: SortFieldCase.createdAt }); } + + setSelectedCases([]); + tableRef.current?.setSelection([]); setFilters(newFilterOptions); refreshCases(false); }, - [refreshCases, setQueryParams, setFilters] + [setSelectedCases, setFilters, refreshCases, setQueryParams] ); const showActions = userCanCrud && !isSelectorView; @@ -319,6 +322,7 @@ export const AllCasesGeneric = React.memo( selection={euiBasicTableSelectionProps} showActions={showActions} sorting={sorting} + tableRef={tableRef} tableRowProps={tableRowProps} userCanCrud={userCanCrud} /> diff --git a/x-pack/plugins/cases/public/components/all_cases/table.tsx b/x-pack/plugins/cases/public/components/all_cases/table.tsx index bc779336b8c019..876007494d276a 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, MutableRefObject } from 'react'; import { EuiEmptyPrompt, EuiLoadingContent, @@ -40,6 +40,7 @@ interface CasesTableProps { selection: EuiTableSelectionType; showActions: boolean; sorting: EuiBasicTableProps['sorting']; + tableRef: MutableRefObject<_EuiBasicTable | undefined>; tableRowProps: EuiBasicTableProps['rowProps']; userCanCrud: boolean; } @@ -92,6 +93,7 @@ export const CasesTable: FunctionComponent = ({ selection, showActions, sorting, + tableRef, tableRowProps, userCanCrud, }) => @@ -110,6 +112,7 @@ export const CasesTable: FunctionComponent = ({ refreshCases={refreshCases} /> = ({ } onChange={onChange} pagination={pagination} + ref={tableRef} rowProps={tableRowProps} selection={showActions ? selection : undefined} sorting={sorting} - className={classnames({ isSelectorView })} /> ); From e9ec16ec97ba5692c4db23fe0193eecddcd22329 Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Thu, 8 Jul 2021 20:16:07 +0200 Subject: [PATCH 18/77] Add additional old SO type from 7.6 to filter and test (#104913) --- src/core/server/saved_objects/migrations/core/elastic_index.ts | 1 + .../migrationsv2/integration_tests/type_registrations.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts index 09c2935422b797..8bda77563be8ca 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.ts @@ -40,6 +40,7 @@ export const REMOVED_TYPES: string[] = [ 'fleet-agent-events', // Was removed in 7.12 'ml-telemetry', + 'server', // https://github.com/elastic/kibana/issues/95617 'tsvb-validation-telemetry', ].sort(); diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts index 47c492622fa3f7..e8871586cdfb78 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts @@ -79,6 +79,7 @@ const previouslyRegisteredTypes = [ 'search-telemetry', 'security-rule', 'security-solution-signals-migration', + 'server', 'siem-detection-engine-rule-actions', 'siem-detection-engine-rule-status', 'siem-ui-timeline', From c77c7fbedbc49039bd4387d1c87d4be0c99350ed Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Thu, 8 Jul 2021 15:24:17 -0400 Subject: [PATCH 19/77] [RAC] [RBAC] MVP RBAC for alerts as data (#100705) An MVP of the RBAC work required for the "alerts as data" effort. An example of the existing implementation for alerts would be that of the security solution. The security solution stores its alerts generated from rules in a single data index - .siem-signals. In order to gain or restrict access to alerts, users do so by following the Elasticsearch privilege architecture. A user would need to go into the Kibana role access UI and give explicit read/write/manage permissions for the index itself. Kibana as a whole is moving away from this model and instead having all user interactions run through the Kibana privilege model. When solutions use saved objects, this authentication layer is abstracted away for them. Because we have chosen to use data indices for alerts, we cannot rely on this abstracted out layer that saved objects provide - we need to provide our own RBAC! Instead of giving users explicit permission to an alerts index, users are instead given access to features. They don't need to know anything about indices, that work we do under the covers now. Co-authored-by: Yara Tercero Co-authored-by: Yara Tercero --- .../src/technical_field_names.ts | 3 + .../src/decode_version/index.ts | 5 +- .../src/encode_hit_version/index.ts | 5 +- .../src/index.ts | 2 + .../alerting_authorization.mock.ts | 3 +- .../alerting_authorization.test.ts | 180 ++++++++ .../authorization/alerting_authorization.ts | 38 +- x-pack/plugins/alerting/server/index.ts | 7 + x-pack/plugins/apm/common/alert_types.ts | 10 +- .../alerting/alerting_flyout/index.tsx | 7 +- x-pack/plugins/apm/server/feature.ts | 81 +++- x-pack/plugins/apm/server/index.ts | 1 + .../alerts/register_error_count_alert_type.ts | 8 +- ...egister_transaction_duration_alert_type.ts | 8 +- ...ister_transaction_error_rate_alert_type.ts | 8 +- .../apm/server/lib/alerts/test_utils/index.ts | 6 +- x-pack/plugins/apm/server/plugin.ts | 5 +- x-pack/plugins/apm/server/routes/typings.ts | 2 + .../server/services/items/create_list_item.ts | 2 +- .../server/services/items/update_list_item.ts | 3 +- .../server/services/lists/create_list.ts | 2 +- .../server/services/lists/update_list.ts | 3 +- .../lists/server/services/utils/index.ts | 2 - .../utils/transform_elastic_to_list.ts | 3 +- .../utils/transform_elastic_to_list_item.ts | 2 +- x-pack/plugins/monitoring/server/types.ts | 2 + x-pack/plugins/observability/server/plugin.ts | 1 + x-pack/plugins/rule_registry/README.md | 29 +- .../field_maps/technical_rule_field_map.ts | 2 + .../plugins/rule_registry/common/constants.ts | 8 + x-pack/plugins/rule_registry/docs/README.md | 44 ++ .../docs/alerts_client/alerts_client_api.md | 14 + .../alerts_client/classes/alertsclient.md | 191 ++++++++ .../interfaces/constructoroptions.md | 52 +++ .../alerts_client/interfaces/updateoptions.md | 58 +++ .../docs/alerts_client_typedoc.json | 17 + x-pack/plugins/rule_registry/kibana.json | 1 + .../alert_data_client/alerts_client.mock.ts | 28 ++ .../server/alert_data_client/alerts_client.ts | 243 ++++++++++ .../alerts_client_factory.test.ts | 78 ++++ .../alerts_client_factory.ts | 55 +++ .../alert_data_client/audit_events.test.ts | 87 ++++ .../server/alert_data_client/audit_events.ts | 61 +++ .../alert_data_client/tests/get.test.ts | 240 ++++++++++ .../alert_data_client/tests/update.test.ts | 376 +++++++++++++++ .../event_log/elasticsearch/index_writer.ts | 2 +- x-pack/plugins/rule_registry/server/index.ts | 1 + x-pack/plugins/rule_registry/server/plugin.ts | 106 ++++- .../routes/__mocks__/request_context.ts | 48 ++ .../routes/__mocks__/request_responses.ts | 27 ++ .../routes/__mocks__/response_adapters.ts | 63 +++ .../server/routes/__mocks__/server.ts | 103 +++++ .../server/routes/get_alert_by_id.test.ts | 98 ++++ .../server/routes/get_alert_by_id.ts | 76 +++ .../server/routes/get_alert_index.ts | 54 +++ .../rule_registry/server/routes/index.ts | 18 + .../server/routes/update_alert_by_id.test.ts | 101 ++++ .../server/routes/update_alert_by_id.ts | 85 ++++ .../server/routes/utils/route_validation.ts | 56 +++ .../server/rule_data_client/types.ts | 9 + .../server/rule_data_plugin_service/index.ts | 6 +- .../rule_data_plugin_service.mock.ts | 35 ++ .../rule_registry/server/scripts/README.md | 24 + .../server/scripts/get_alerts_index.sh | 23 + .../server/scripts/get_observability_alert.sh | 22 + .../server/scripts/get_security_alert.sh | 22 + .../server/scripts/hunter/README.md | 5 + .../scripts/hunter/delete_detections_user.sh | 11 + .../scripts/hunter/detections_role.json | 19 + .../scripts/hunter/detections_user.json | 6 + .../scripts/hunter/get_detections_role.sh | 11 + .../server/scripts/hunter/index.ts | 10 + .../scripts/hunter/post_detections_role.sh | 14 + .../scripts/hunter/post_detections_user.sh | 14 + .../server/scripts/observer/README.md | 5 + .../observer/delete_detections_user.sh | 11 + .../scripts/observer/detections_role.json | 20 + .../scripts/observer/detections_user.json | 6 + .../scripts/observer/get_detections_role.sh | 11 + .../observer/get_observability_alert.sh | 21 + .../observer/get_security_solution_alert.sh | 22 + .../server/scripts/observer/index.ts | 10 + .../scripts/observer/post_detections_role.sh | 14 + .../scripts/observer/post_detections_user.sh | 14 + .../scripts/update_observability_alert.sh | 28 ++ x-pack/plugins/rule_registry/server/types.ts | 16 + .../utils/create_lifecycle_rule_type.test.ts | 4 + .../create_lifecycle_rule_type_factory.ts | 4 + .../rule_registry/server/utils/rbac.ts | 22 + x-pack/plugins/rule_registry/tsconfig.json | 10 +- .../security_solution/server/plugin.ts | 5 +- x-pack/scripts/functional_tests.js | 3 + .../apis/security/privileges.ts | 32 +- .../apis/security_solution/events.ts | 72 ++- .../tests/alerts/rule_registry.ts | 9 + .../rule_registry/alerts/data.json | 29 ++ .../rule_registry/alerts/mappings.json | 47 ++ x-pack/test/rule_registry/common/config.ts | 94 ++++ .../common/ftr_provider_context.d.ts | 12 + .../common/lib/authentication/index.ts | 105 +++++ .../common/lib/authentication/roles.ts | 435 ++++++++++++++++++ .../common/lib/authentication/spaces.ts | 26 ++ .../common/lib/authentication/types.ts | 54 +++ .../common/lib/authentication/users.ts | 301 ++++++++++++ x-pack/test/rule_registry/common/services.ts | 8 + .../security_and_spaces/config_basic.ts | 15 + .../security_and_spaces/config_trial.ts | 15 + .../roles_users_utils/index.ts | 124 +++++ .../tests/basic/get_alert_by_id.ts | 210 +++++++++ .../security_and_spaces/tests/basic/index.ts | 29 ++ .../tests/basic/update_alert.ts | 251 ++++++++++ .../tests/trial/get_alerts.ts | 127 +++++ .../security_and_spaces/tests/trial/index.ts | 104 +++++ .../tests/trial/update_alert.ts | 189 ++++++++ .../rule_registry/spaces_only/config_trial.ts | 16 + .../tests/trial/get_alert_by_id.ts | 87 ++++ .../spaces_only/tests/trial/index.ts | 29 ++ .../spaces_only/tests/trial/update_alert.ts | 108 +++++ 118 files changed, 5700 insertions(+), 106 deletions(-) rename x-pack/plugins/lists/server/services/utils/decode_version.ts => packages/kbn-securitysolution-es-utils/src/decode_version/index.ts (85%) rename x-pack/plugins/lists/server/services/utils/encode_hit_version.ts => packages/kbn-securitysolution-es-utils/src/encode_hit_version/index.ts (84%) create mode 100644 x-pack/plugins/rule_registry/common/constants.ts create mode 100644 x-pack/plugins/rule_registry/docs/README.md create mode 100644 x-pack/plugins/rule_registry/docs/alerts_client/alerts_client_api.md create mode 100644 x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md create mode 100644 x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md create mode 100644 x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md create mode 100644 x-pack/plugins/rule_registry/docs/alerts_client_typedoc.json create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/audit_events.test.ts create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/audit_events.ts create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts create mode 100644 x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts create mode 100644 x-pack/plugins/rule_registry/server/routes/__mocks__/request_context.ts create mode 100644 x-pack/plugins/rule_registry/server/routes/__mocks__/request_responses.ts create mode 100644 x-pack/plugins/rule_registry/server/routes/__mocks__/response_adapters.ts create mode 100644 x-pack/plugins/rule_registry/server/routes/__mocks__/server.ts create mode 100644 x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts create mode 100644 x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts create mode 100644 x-pack/plugins/rule_registry/server/routes/get_alert_index.ts create mode 100644 x-pack/plugins/rule_registry/server/routes/index.ts create mode 100644 x-pack/plugins/rule_registry/server/routes/update_alert_by_id.test.ts create mode 100644 x-pack/plugins/rule_registry/server/routes/update_alert_by_id.ts create mode 100644 x-pack/plugins/rule_registry/server/routes/utils/route_validation.ts create mode 100644 x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts create mode 100644 x-pack/plugins/rule_registry/server/scripts/README.md create mode 100755 x-pack/plugins/rule_registry/server/scripts/get_alerts_index.sh create mode 100755 x-pack/plugins/rule_registry/server/scripts/get_observability_alert.sh create mode 100755 x-pack/plugins/rule_registry/server/scripts/get_security_alert.sh create mode 100644 x-pack/plugins/rule_registry/server/scripts/hunter/README.md create mode 100755 x-pack/plugins/rule_registry/server/scripts/hunter/delete_detections_user.sh create mode 100644 x-pack/plugins/rule_registry/server/scripts/hunter/detections_role.json create mode 100644 x-pack/plugins/rule_registry/server/scripts/hunter/detections_user.json create mode 100755 x-pack/plugins/rule_registry/server/scripts/hunter/get_detections_role.sh create mode 100644 x-pack/plugins/rule_registry/server/scripts/hunter/index.ts create mode 100755 x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_role.sh create mode 100755 x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_user.sh create mode 100644 x-pack/plugins/rule_registry/server/scripts/observer/README.md create mode 100755 x-pack/plugins/rule_registry/server/scripts/observer/delete_detections_user.sh create mode 100644 x-pack/plugins/rule_registry/server/scripts/observer/detections_role.json create mode 100644 x-pack/plugins/rule_registry/server/scripts/observer/detections_user.json create mode 100755 x-pack/plugins/rule_registry/server/scripts/observer/get_detections_role.sh create mode 100755 x-pack/plugins/rule_registry/server/scripts/observer/get_observability_alert.sh create mode 100755 x-pack/plugins/rule_registry/server/scripts/observer/get_security_solution_alert.sh create mode 100644 x-pack/plugins/rule_registry/server/scripts/observer/index.ts create mode 100755 x-pack/plugins/rule_registry/server/scripts/observer/post_detections_role.sh create mode 100755 x-pack/plugins/rule_registry/server/scripts/observer/post_detections_user.sh create mode 100755 x-pack/plugins/rule_registry/server/scripts/update_observability_alert.sh create mode 100644 x-pack/plugins/rule_registry/server/utils/rbac.ts create mode 100644 x-pack/test/functional/es_archives/rule_registry/alerts/data.json create mode 100644 x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json create mode 100644 x-pack/test/rule_registry/common/config.ts create mode 100644 x-pack/test/rule_registry/common/ftr_provider_context.d.ts create mode 100644 x-pack/test/rule_registry/common/lib/authentication/index.ts create mode 100644 x-pack/test/rule_registry/common/lib/authentication/roles.ts create mode 100644 x-pack/test/rule_registry/common/lib/authentication/spaces.ts create mode 100644 x-pack/test/rule_registry/common/lib/authentication/types.ts create mode 100644 x-pack/test/rule_registry/common/lib/authentication/users.ts create mode 100644 x-pack/test/rule_registry/common/services.ts create mode 100644 x-pack/test/rule_registry/security_and_spaces/config_basic.ts create mode 100644 x-pack/test/rule_registry/security_and_spaces/config_trial.ts create mode 100644 x-pack/test/rule_registry/security_and_spaces/roles_users_utils/index.ts create mode 100644 x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts create mode 100644 x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts create mode 100644 x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts create mode 100644 x-pack/test/rule_registry/security_and_spaces/tests/trial/get_alerts.ts create mode 100644 x-pack/test/rule_registry/security_and_spaces/tests/trial/index.ts create mode 100644 x-pack/test/rule_registry/security_and_spaces/tests/trial/update_alert.ts create mode 100644 x-pack/test/rule_registry/spaces_only/config_trial.ts create mode 100644 x-pack/test/rule_registry/spaces_only/tests/trial/get_alert_by_id.ts create mode 100644 x-pack/test/rule_registry/spaces_only/tests/trial/index.ts create mode 100644 x-pack/test/rule_registry/spaces_only/tests/trial/update_alert.ts diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts index 31779c9f08e819..6c45403fc0a136 100644 --- a/packages/kbn-rule-data-utils/src/technical_field_names.ts +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -19,6 +19,7 @@ const RULE_NAME = 'rule.name' as const; const RULE_CATEGORY = 'rule.category' as const; const TAGS = 'tags' as const; const PRODUCER = `${ALERT_NAMESPACE}.producer` as const; +const OWNER = `${ALERT_NAMESPACE}.owner` as const; const ALERT_ID = `${ALERT_NAMESPACE}.id` as const; const ALERT_UUID = `${ALERT_NAMESPACE}.uuid` as const; const ALERT_START = `${ALERT_NAMESPACE}.start` as const; @@ -40,6 +41,7 @@ const fields = { RULE_CATEGORY, TAGS, PRODUCER, + OWNER, ALERT_ID, ALERT_UUID, ALERT_START, @@ -62,6 +64,7 @@ export { RULE_CATEGORY, TAGS, PRODUCER, + OWNER, ALERT_ID, ALERT_UUID, ALERT_START, diff --git a/x-pack/plugins/lists/server/services/utils/decode_version.ts b/packages/kbn-securitysolution-es-utils/src/decode_version/index.ts similarity index 85% rename from x-pack/plugins/lists/server/services/utils/decode_version.ts rename to packages/kbn-securitysolution-es-utils/src/decode_version/index.ts index 8ed934204ed982..d58c7add67a27f 100644 --- a/x-pack/plugins/lists/server/services/utils/decode_version.ts +++ b/packages/kbn-securitysolution-es-utils/src/decode_version/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ // Similar to the src/core/server/saved_objects/version/decode_version.ts diff --git a/x-pack/plugins/lists/server/services/utils/encode_hit_version.ts b/packages/kbn-securitysolution-es-utils/src/encode_hit_version/index.ts similarity index 84% rename from x-pack/plugins/lists/server/services/utils/encode_hit_version.ts rename to packages/kbn-securitysolution-es-utils/src/encode_hit_version/index.ts index 4c55d858d283b4..29b5a18f7c303b 100644 --- a/x-pack/plugins/lists/server/services/utils/encode_hit_version.ts +++ b/packages/kbn-securitysolution-es-utils/src/encode_hit_version/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ /** diff --git a/packages/kbn-securitysolution-es-utils/src/index.ts b/packages/kbn-securitysolution-es-utils/src/index.ts index cfa6820e9aac52..8dead7f899ba2d 100644 --- a/packages/kbn-securitysolution-es-utils/src/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/index.ts @@ -8,10 +8,12 @@ export * from './bad_request_error'; export * from './create_boostrap_index'; +export * from './decode_version'; export * from './delete_all_index'; export * from './delete_policy'; export * from './delete_template'; export * from './elasticsearch_client'; +export * from './encode_hit_version'; export * from './get_index_aliases'; export * from './get_index_count'; export * from './get_index_exists'; diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts index 4e4cd4419a5a22..5e3dd2019d0a0f 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts @@ -16,12 +16,13 @@ const createAlertingAuthorizationMock = () => { ensureAuthorized: jest.fn(), filterByRuleTypeAuthorization: jest.fn(), getFindAuthorizationFilter: jest.fn(), + getAugmentedRuleTypesWithAuthorization: jest.fn(), }; return mocked; }; export const alertingAuthorizationMock: { - create: () => AlertingAuthorizationMock; + create: () => jest.Mocked>; } = { create: createAlertingAuthorizationMock, }; diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts index c07148f03c6849..4b1fc7f1a7ccb3 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts @@ -1944,4 +1944,184 @@ describe('AlertingAuthorization', () => { `); }); }); + + describe('getAugmentedRuleTypesWithAuthorization', () => { + const myOtherAppAlertType: RegistryAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + recoveryActionGroup: RecoveredActionGroup, + id: 'myOtherAppAlertType', + name: 'myOtherAppAlertType', + producer: 'alerts', + enabledInLicense: true, + isExportable: true, + }; + const myAppAlertType: RegistryAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + recoveryActionGroup: RecoveredActionGroup, + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + enabledInLicense: true, + isExportable: true, + }; + const mySecondAppAlertType: RegistryAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + recoveryActionGroup: RecoveredActionGroup, + id: 'mySecondAppAlertType', + name: 'mySecondAppAlertType', + producer: 'myApp', + enabledInLicense: true, + isExportable: true, + }; + const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]); + + test('it returns authorized rule types given a set of feature ids', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'find'), + authorized: true, + }, + ], + }, + }); + const alertAuthorization = new AlertingAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + auditLogger, + getSpace, + exemptConsumerIds, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.getAugmentedRuleTypesWithAuthorization( + ['myApp'], + [ReadOperations.Find, ReadOperations.Get, WriteOperations.Update], + AlertingAuthorizationEntity.Alert + ) + ).resolves.toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": false, + "read": true, + }, + }, + "defaultActionGroupId": "default", + "enabledInLicense": true, + "id": "myOtherAppAlertType", + "isExportable": true, + "minimumLicenseRequired": "basic", + "name": "myOtherAppAlertType", + "producer": "alerts", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", + }, + }, + }, + "hasAllRequested": false, + "username": "some-user", + } + `); + }); + + test('it returns all authorized if user has read, get and update alert privileges', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'update'), + authorized: true, + }, + ], + }, + }); + const alertAuthorization = new AlertingAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + auditLogger, + getSpace, + exemptConsumerIds, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.getAugmentedRuleTypesWithAuthorization( + ['myApp'], + [ReadOperations.Find, ReadOperations.Get, WriteOperations.Update], + AlertingAuthorizationEntity.Alert + ) + ).resolves.toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": true, + "read": true, + }, + }, + "defaultActionGroupId": "default", + "enabledInLicense": true, + "id": "myOtherAppAlertType", + "isExportable": true, + "minimumLicenseRequired": "basic", + "name": "myOtherAppAlertType", + "producer": "alerts", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", + }, + }, + }, + "hasAllRequested": false, + "username": "some-user", + } + `); + }); + }); }); diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index 52cef9a402e352..50a1b9d84ff6db 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -124,20 +124,41 @@ export class AlertingAuthorization { return new Set(); }); - this.allPossibleConsumers = this.featuresIds.then((featuresIds) => - featuresIds.size + this.allPossibleConsumers = this.featuresIds.then((featuresIds) => { + return featuresIds.size ? asAuthorizedConsumers([...this.exemptConsumerIds, ...featuresIds], { read: true, all: true, }) - : {} - ); + : {}; + }); } private shouldCheckAuthorization(): boolean { return this.authorization?.mode?.useRbacForRequest(this.request) ?? false; } + /* + * This method exposes the private 'augmentRuleTypesWithAuthorization' to be + * used by the RAC/Alerts client + */ + public async getAugmentedRuleTypesWithAuthorization( + featureIds: readonly string[], + operations: Array, + authorizationEntity: AlertingAuthorizationEntity + ): Promise<{ + username?: string; + hasAllRequested: boolean; + authorizedRuleTypes: Set; + }> { + return this.augmentRuleTypesWithAuthorization( + this.alertTypeRegistry.list(), + operations, + authorizationEntity, + new Set(featureIds) + ); + } + public async ensureAuthorized({ ruleTypeId, consumer, operation, entity }: EnsureAuthorizedOpts) { const { authorization } = this; @@ -339,13 +360,14 @@ export class AlertingAuthorization { private async augmentRuleTypesWithAuthorization( ruleTypes: Set, operations: Array, - authorizationEntity: AlertingAuthorizationEntity + authorizationEntity: AlertingAuthorizationEntity, + featuresIds?: Set ): Promise<{ username?: string; hasAllRequested: boolean; authorizedRuleTypes: Set; }> { - const featuresIds = await this.featuresIds; + const fIds = featuresIds ?? (await this.featuresIds); if (this.authorization && this.shouldCheckAuthorization()) { const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( this.request @@ -363,7 +385,7 @@ export class AlertingAuthorization { // as we can't ask ES for the user's individual privileges we need to ask for each feature // and ruleType in the system whether this user has this privilege for (const ruleType of ruleTypesWithAuthorization) { - for (const feature of featuresIds) { + for (const feature of fIds) { for (const operation of operations) { privilegeToRuleType.set( this.authorization!.actions.alerting.get( @@ -420,7 +442,7 @@ export class AlertingAuthorization { return { hasAllRequested: true, authorizedRuleTypes: this.augmentWithAuthorizedConsumers( - new Set([...ruleTypes].filter((ruleType) => featuresIds.has(ruleType.producer))), + new Set([...ruleTypes].filter((ruleType) => fIds.has(ruleType.producer))), await this.allPossibleConsumers ), }; diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 72e3325107f316..957bd89f52f36c 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -34,6 +34,13 @@ export { FindResult } from './alerts_client'; export { PublicAlertInstance as AlertInstance } from './alert_instance'; export { parseDuration } from './lib'; export { getEsErrorMessage } from './lib/errors'; +export { + ReadOperations, + AlertingAuthorizationFilterType, + AlertingAuthorization, + WriteOperations, + AlertingAuthorizationEntity, +} from './authorization'; export const plugin = (initContext: PluginInitializerContext) => new AlertingPlugin(initContext); diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index ad233c7f6df921..9476396a7aefa6 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -10,6 +10,8 @@ import type { ValuesType } from 'utility-types'; import type { ActionGroup } from '../../alerting/common'; import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from './ml_constants'; +export const APM_SERVER_FEATURE_ID = 'apm'; + export enum AlertType { ErrorCount = 'apm.error_rate', // ErrorRate was renamed to ErrorCount but the key is kept as `error_rate` for backwards-compat. TransactionErrorRate = 'apm.transaction_error_rate', @@ -44,7 +46,7 @@ export const ALERT_TYPES_CONFIG: Record< actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, isExportable: true, }, [AlertType.TransactionDuration]: { @@ -54,7 +56,7 @@ export const ALERT_TYPES_CONFIG: Record< actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, isExportable: true, }, [AlertType.TransactionDurationAnomaly]: { @@ -64,7 +66,7 @@ export const ALERT_TYPES_CONFIG: Record< actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, isExportable: true, }, [AlertType.TransactionErrorRate]: { @@ -74,7 +76,7 @@ export const ALERT_TYPES_CONFIG: Record< actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, isExportable: true, }, }; diff --git a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx index 35863d80993944..b87298c5fe8a09 100644 --- a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx @@ -8,7 +8,10 @@ import React, { useCallback, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { AlertType } from '../../../../common/alert_types'; +import { + AlertType, + APM_SERVER_FEATURE_ID, +} from '../../../../common/alert_types'; import { getInitialAlertValues } from '../get_initial_alert_values'; import { ApmPluginStartDeps } from '../../../plugin'; interface Props { @@ -31,7 +34,7 @@ export function AlertingFlyout(props: Props) { () => alertType && services.triggersActionsUi.getAddAlertFlyout({ - consumer: 'apm', + consumer: APM_SERVER_FEATURE_ID, onClose: onCloseAddFlyout, alertTypeId: alertType, canChangeTrigger: false, diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index fb0610dffb92e4..f3e2bba2d9789d 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -6,8 +6,9 @@ */ import { i18n } from '@kbn/i18n'; +import { SubFeaturePrivilegeGroupType } from '../../features/common'; import { LicenseType } from '../../licensing/common/types'; -import { AlertType } from '../common/alert_types'; +import { AlertType, APM_SERVER_FEATURE_ID } from '../common/alert_types'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { LicensingPluginSetup, @@ -15,14 +16,14 @@ import { } from '../../licensing/server'; export const APM_FEATURE = { - id: 'apm', + id: APM_SERVER_FEATURE_ID, name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', { defaultMessage: 'APM and User Experience', }), order: 900, category: DEFAULT_APP_CATEGORIES.observability, - app: ['apm', 'ux', 'kibana'], - catalogue: ['apm'], + app: [APM_SERVER_FEATURE_ID, 'ux', 'kibana'], + catalogue: [APM_SERVER_FEATURE_ID], management: { insightsAndAlerting: ['triggersActions'], }, @@ -30,9 +31,9 @@ export const APM_FEATURE = { // see x-pack/plugins/features/common/feature_kibana_privileges.ts privileges: { all: { - app: ['apm', 'ux', 'kibana'], - api: ['apm', 'apm_write'], - catalogue: ['apm'], + app: [APM_SERVER_FEATURE_ID, 'ux', 'kibana'], + api: [APM_SERVER_FEATURE_ID, 'apm_write', 'rac'], + catalogue: [APM_SERVER_FEATURE_ID], savedObject: { all: [], read: [], @@ -41,9 +42,6 @@ export const APM_FEATURE = { rule: { all: Object.values(AlertType), }, - alert: { - all: Object.values(AlertType), - }, }, management: { insightsAndAlerting: ['triggersActions'], @@ -51,9 +49,9 @@ export const APM_FEATURE = { ui: ['show', 'save', 'alerting:show', 'alerting:save'], }, read: { - app: ['apm', 'ux', 'kibana'], - api: ['apm'], - catalogue: ['apm'], + app: [APM_SERVER_FEATURE_ID, 'ux', 'kibana'], + api: [APM_SERVER_FEATURE_ID, 'rac'], + catalogue: [APM_SERVER_FEATURE_ID], savedObject: { all: [], read: [], @@ -62,9 +60,6 @@ export const APM_FEATURE = { rule: { read: Object.values(AlertType), }, - alert: { - read: Object.values(AlertType), - }, }, management: { insightsAndAlerting: ['triggersActions'], @@ -72,6 +67,60 @@ export const APM_FEATURE = { ui: ['show', 'alerting:show', 'alerting:save'], }, }, + subFeatures: [ + { + name: i18n.translate('xpack.apm.featureRegistry.manageAlertsName', { + defaultMessage: 'Alerts', + }), + privilegeGroups: [ + { + groupType: 'mutually_exclusive' as SubFeaturePrivilegeGroupType, + privileges: [ + { + id: 'alerts_all', + name: i18n.translate( + 'xpack.apm.featureRegistry.subfeature.alertsAllName', + { + defaultMessage: 'All', + } + ), + includeIn: 'all' as 'all', + alerting: { + alert: { + all: Object.values(AlertType), + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + { + id: 'alerts_read', + name: i18n.translate( + 'xpack.apm.featureRegistry.subfeature.alertsReadName', + { + defaultMessage: 'Read', + } + ), + includeIn: 'read' as 'read', + alerting: { + alert: { + read: Object.values(AlertType), + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + ], + }, + ], + }, + ], }; interface Feature { diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 8ec92bfa7a1b58..f14894a76edb45 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -125,6 +125,7 @@ export function mergeConfigs( export const plugin = (initContext: PluginInitializerContext) => new APMPlugin(initContext); +export { APM_SERVER_FEATURE_ID } from '../common/alert_types'; export { APMPlugin } from './plugin'; export { APMPluginSetup } from './types'; export { APMServerRouteRepository } from './routes/get_global_apm_server_route_repository'; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 7548d6eba060aa..35c80df2ca31cd 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -17,7 +17,11 @@ import { getEnvironmentEsField, getEnvironmentLabel, } from '../../../common/environment_filter_values'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { + AlertType, + APM_SERVER_FEATURE_ID, + ALERT_TYPES_CONFIG, +} from '../../../common/alert_types'; import { PROCESSOR_EVENT, SERVICE_ENVIRONMENT, @@ -69,7 +73,7 @@ export function registerErrorCountAlertType({ apmActionVariables.interval, ], }, - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', isExportable: true, executor: async ({ services, params }) => { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index ca7806251f75e5..ff202669fe1da5 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -17,7 +17,11 @@ import { getEnvironmentLabel, getEnvironmentEsField, } from '../../../common/environment_filter_values'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { + AlertType, + APM_SERVER_FEATURE_ID, + ALERT_TYPES_CONFIG, +} from '../../../common/alert_types'; import { PROCESSOR_EVENT, SERVICE_NAME, @@ -77,7 +81,7 @@ export function registerTransactionDurationAlertType({ apmActionVariables.interval, ], }, - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', isExportable: true, executor: async ({ services, params }) => { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index 718ffd9c92167a..36fd9c3fac58d8 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -17,7 +17,11 @@ import { getEnvironmentLabel, } from '../../../common/environment_filter_values'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { + AlertType, + ALERT_TYPES_CONFIG, + APM_SERVER_FEATURE_ID, +} from '../../../common/alert_types'; import { EVENT_OUTCOME, PROCESSOR_EVENT, @@ -75,7 +79,7 @@ export function registerTransactionErrorRateAlertType({ apmActionVariables.interval, ], }, - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', isExportable: true, executor: async ({ services, params: alertParams }) => { diff --git a/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts b/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts index 9dc22844bb629f..1366503ea14284 100644 --- a/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts +++ b/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts @@ -10,7 +10,7 @@ import { of } from 'rxjs'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; import type { RuleDataClient } from '../../../../../rule_registry/server'; import { PluginSetupContract as AlertingPluginSetupContract } from '../../../../../alerting/server'; -import { APMConfig } from '../../..'; +import { APMConfig, APM_SERVER_FEATURE_ID } from '../../..'; export const createRuleTypeMocks = () => { let alertExecutor: (...args: any[]) => Promise; @@ -38,6 +38,9 @@ export const createRuleTypeMocks = () => { const services = { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClient: { + get: () => ({ attributes: { consumer: APM_SERVER_FEATURE_ID } }), + }, alertInstanceFactory: jest.fn(() => ({ scheduleActions })), alertWithLifecycle: jest.fn(), logger: loggerMock, @@ -67,6 +70,7 @@ export const createRuleTypeMocks = () => { executor: async ({ params }: { params: Record }) => { return alertExecutor({ services, + rule: { consumer: APM_SERVER_FEATURE_ID }, params, startedAt: new Date(), }); diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 647330eade1f5c..f260971c3bdcbd 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -18,7 +18,7 @@ import { import { mapValues, once } from 'lodash'; import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../rule_registry/common/assets'; import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map'; -import { APMConfig, APMXPackConfig } from '.'; +import { APMConfig, APMXPackConfig, APM_SERVER_FEATURE_ID } from '.'; import { mergeConfigs } from './index'; import { UI_SETTINGS } from '../../../../src/plugins/data/common'; import { APM_FEATURE, registerFeaturesUsage } from './feature'; @@ -188,6 +188,7 @@ export class APMPlugin ); const ruleDataClient = ruleDataService.getRuleDataClient( + APM_SERVER_FEATURE_ID, ruleDataService.getFullAssetName('observability-apm'), () => initializeRuleDataTemplatesPromise ); @@ -206,7 +207,7 @@ export class APMPlugin }) as APMRouteHandlerResources['plugins']; const telemetryUsageCounter = resourcePlugins.usageCollection?.setup.createUsageCounter( - 'apm' + APM_SERVER_FEATURE_ID ); registerRoutes({ diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 98c2ee47b5633b..56a5950c273673 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -14,6 +14,7 @@ import { } from 'src/core/server'; import { RuleDataClient } from '../../../rule_registry/server'; import { AlertingApiRequestHandlerContext } from '../../../alerting/server'; +import type { RacApiRequestHandlerContext } from '../../../rule_registry/server'; import { LicensingApiRequestHandlerContext } from '../../../licensing/server'; import { APMConfig } from '..'; import { APMPluginDependencies } from '../types'; @@ -21,6 +22,7 @@ import { APMPluginDependencies } from '../types'; export interface ApmPluginRequestHandlerContext extends RequestHandlerContext { licensing: LicensingApiRequestHandlerContext; alerting: AlertingApiRequestHandlerContext; + rac: RacApiRequestHandlerContext; } export type InspectResponse = Array<{ diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts index b4203f000b7b97..ccdb8ab4779b69 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -15,9 +15,9 @@ import { SerializerOrUndefined, Type, } from '@kbn/securitysolution-io-ts-list-types'; +import { encodeHitVersion } from '@kbn/securitysolution-es-utils'; import { transformListItemToElasticQuery } from '../utils'; -import { encodeHitVersion } from '../utils/encode_hit_version'; import { IndexEsListItemSchema } from '../../schemas/elastic_query'; export interface CreateListItemOptions { diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts index c73149019f4168..78651bb83d73b0 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -12,10 +12,9 @@ import type { MetaOrUndefined, _VersionOrUndefined, } from '@kbn/securitysolution-io-ts-list-types'; +import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils'; import { transformListItemToElasticQuery } from '../utils'; -import { decodeVersion } from '../utils/decode_version'; -import { encodeHitVersion } from '../utils/encode_hit_version'; import { UpdateEsListItemSchema } from '../../schemas/elastic_query'; import { getListItem } from './get_list_item'; diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts index 6c7081d7c701ec..521a38a51d6eb0 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.ts @@ -19,8 +19,8 @@ import type { Type, } from '@kbn/securitysolution-io-ts-list-types'; import type { Version } from '@kbn/securitysolution-io-ts-types'; +import { encodeHitVersion } from '@kbn/securitysolution-es-utils'; -import { encodeHitVersion } from '../utils/encode_hit_version'; import { IndexEsListSchema } from '../../schemas/elastic_query'; export interface CreateListOptions { diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts index 22235341ca0755..11868a6187bbf8 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -15,9 +15,8 @@ import type { _VersionOrUndefined, } from '@kbn/securitysolution-io-ts-list-types'; import { VersionOrUndefined } from '@kbn/securitysolution-io-ts-types'; +import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils'; -import { decodeVersion } from '../utils/decode_version'; -import { encodeHitVersion } from '../utils/encode_hit_version'; import { UpdateEsListSchema } from '../../schemas/elastic_query'; import { getList } from '.'; diff --git a/x-pack/plugins/lists/server/services/utils/index.ts b/x-pack/plugins/lists/server/services/utils/index.ts index 0cd2720bd199b8..64e7c50d0e7b0e 100644 --- a/x-pack/plugins/lists/server/services/utils/index.ts +++ b/x-pack/plugins/lists/server/services/utils/index.ts @@ -6,9 +6,7 @@ */ export * from './calculate_scroll_math'; -export * from './decode_version'; export * from './encode_decode_cursor'; -export * from './encode_hit_version'; export * from './escape_query'; export * from './find_source_type'; export * from './find_source_value'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts index 19177c1c2785ff..5b0949d7b79b7a 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts @@ -7,11 +7,10 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ListArraySchema } from '@kbn/securitysolution-io-ts-list-types'; +import { encodeHitVersion } from '@kbn/securitysolution-es-utils'; import { SearchEsListSchema } from '../../schemas/elastic_response'; -import { encodeHitVersion } from './encode_hit_version'; - export interface TransformElasticToListOptions { response: estypes.SearchResponse; } diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts index 79db56f9a7fe9e..65392f8c379d95 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts @@ -7,11 +7,11 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ListItemArraySchema, Type } from '@kbn/securitysolution-io-ts-list-types'; +import { encodeHitVersion } from '@kbn/securitysolution-es-utils'; import { ErrorWithStatusCode } from '../../error_with_status_code'; import { SearchEsListItemSchema } from '../../schemas/elastic_response'; -import { encodeHitVersion } from './encode_hit_version'; import { findSourceValue } from './find_source_value'; export interface TransformElasticToListItemOptions { diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index c4a0687bef497b..b920f2bfacf80f 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -20,6 +20,7 @@ import type { ActionsApiRequestHandlerContext, } from '../../actions/server'; import type { AlertingApiRequestHandlerContext } from '../../alerting/server'; +import type { RacApiRequestHandlerContext } from '../../rule_registry/server'; import { PluginStartContract as AlertingPluginStartContract, PluginSetupContract as AlertingPluginSetupContract, @@ -57,6 +58,7 @@ export interface RequestHandlerContextMonitoringPlugin extends RequestHandlerCon actions?: ActionsApiRequestHandlerContext; alerting?: AlertingApiRequestHandlerContext; infra: InfraRequestHandlerContext; + ruleRegistry?: RacApiRequestHandlerContext; } export interface PluginsStart { diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index d820a6c0a6f761..3e8f511eb11534 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -99,6 +99,7 @@ export class ObservabilityPlugin implements Plugin { const start = () => core.getStartServices().then(([coreStart]) => coreStart); const ruleDataClient = plugins.ruleRegistry.ruleDataService.getRuleDataClient( + 'observability', plugins.ruleRegistry.ruleDataService.getFullAssetName(), () => Promise.resolve() ); diff --git a/x-pack/plugins/rule_registry/README.md b/x-pack/plugins/rule_registry/README.md index 3fe6305a0d9f6e..945b8f161eb84e 100644 --- a/x-pack/plugins/rule_registry/README.md +++ b/x-pack/plugins/rule_registry/README.md @@ -27,9 +27,7 @@ On plugin setup, rule type producers can create the index template as follows: ```ts // get the FQN of the component template. All assets are prefixed with the configured `index` value, which is `.alerts` by default. -const componentTemplateName = plugins.ruleRegistry.getFullAssetName( - 'apm-mappings' -); +const componentTemplateName = plugins.ruleRegistry.getFullAssetName('apm-mappings'); // if write is disabled, don't install these templates if (!plugins.ruleRegistry.isWriteEnabled()) { @@ -73,14 +71,10 @@ await plugins.ruleRegistry.createOrUpdateComponentTemplate({ await plugins.ruleRegistry.createOrUpdateIndexTemplate({ name: plugins.ruleRegistry.getFullAssetName('apm-index-template'), body: { - index_patterns: [ - plugins.ruleRegistry.getFullAssetName('observability-apm*'), - ], + index_patterns: [plugins.ruleRegistry.getFullAssetName('observability-apm*')], composed_of: [ // Technical component template, required - plugins.ruleRegistry.getFullAssetName( - TECHNICAL_COMPONENT_TEMPLATE_NAME - ), + plugins.ruleRegistry.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME), componentTemplateName, ], }, @@ -107,8 +101,7 @@ await ruleDataClient.getWriter().bulk({ // to read data, simply call ruleDataClient.getReader().search: const response = await ruleDataClient.getReader().search({ body: { - query: { - }, + query: {}, size: 100, fields: ['*'], sort: { @@ -132,6 +125,7 @@ The following fields are defined in the technical field component template and s - `rule.name`: the name of the rule (as specified by the user). - `rule.category`: the name of the rule type (as defined by the rule type producer) - `kibana.rac.alert.producer`: the producer of the rule type. Usually a Kibana plugin. e.g., `APM`. +- `kibana.rac.alert.owner`: the feature which produced the alert. Usually a Kibana feature id like `apm`, `siem`... - `kibana.rac.alert.id`: the id of the alert, that is unique within the context of the rule execution it was created in. E.g., for a rule that monitors latency for all services in all environments, this might be `opbeans-java:production`. - `kibana.rac.alert.uuid`: the unique identifier for the alert during its lifespan. If an alert recovers (or closes), this identifier is re-generated when it is opened again. - `kibana.rac.alert.status`: the status of the alert. Can be `open` or `closed`. @@ -145,3 +139,16 @@ The following fields are defined in the technical field component template and s - `kibana.rac.alert.ancestors`: the array of ancestors (if any) for the alert. - `kibana.rac.alert.depth`: the depth of the alert in the ancestral tree (default 0). - `kibana.rac.alert.building_block_type`: the building block type of the alert (default undefined). + +# Alerts as data + +Alerts as data can be interacted with using the AlertsClient api found in `x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts` + +This api includes public methods such as + +[x] getFullAssetName +[x] getAlertsIndex +[x] get +[x] update +[ ] bulkUpdate (TODO) +[ ] find (TODO) diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts index a946e9523548ca..6d70c581802c13 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts @@ -18,6 +18,7 @@ import { ALERT_UUID, EVENT_ACTION, EVENT_KIND, + OWNER, PRODUCER, RULE_CATEGORY, RULE_ID, @@ -40,6 +41,7 @@ export const technicalRuleFieldMap = { RULE_CATEGORY, TAGS ), + [OWNER]: { type: 'keyword' }, [PRODUCER]: { type: 'keyword' }, [ALERT_UUID]: { type: 'keyword' }, [ALERT_ID]: { type: 'keyword' }, diff --git a/x-pack/plugins/rule_registry/common/constants.ts b/x-pack/plugins/rule_registry/common/constants.ts new file mode 100644 index 00000000000000..72793b1087e7b3 --- /dev/null +++ b/x-pack/plugins/rule_registry/common/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const BASE_RAC_ALERTS_API_PATH = '/internal/rac/alerts'; diff --git a/x-pack/plugins/rule_registry/docs/README.md b/x-pack/plugins/rule_registry/docs/README.md new file mode 100644 index 00000000000000..a22dc1ab7e8642 --- /dev/null +++ b/x-pack/plugins/rule_registry/docs/README.md @@ -0,0 +1,44 @@ +# Alerts as data Client API Docs + +This directory contains generated docs using `typedoc` for the alerts as data client (alerts client) API that can be called from other server +plugins. This README will describe how to generate a new version of these markdown docs in the event that new methods +or parameters are added. + +## TypeDoc Info + +See more info at: +and: for the markdown plugin + +## Install dependencies + +```bash +yarn global add typedoc typedoc-plugin-markdown +``` + +## Generate the docs + +```bash +cd x-pack/plugins/rule_registry/docs +npx typedoc --options alerts_client_typedoc.json +``` + +After running the above commands the files in the `server` directory will be updated to match the new tsdocs. +If additional markdown directory should be created we can create a new typedoc configuration file and adjust the `out` +directory accordingly. + +## Troubleshooting + +This will use the global `tsc` so ensure typescript is installed globally and one of typescript version `3.9, 4.0, 4.1, 4.2`. + +``` +$ tsc --version +Version 4.2.4 +``` + +If you run into tsc errors that seem unrelated to the cases plugin try executing these commands before running `typedoc` + +```bash +cd +npx yarn kbn bootstrap +node scripts/build_ts_refs.js --clean --no-cache +``` diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/alerts_client_api.md b/x-pack/plugins/rule_registry/docs/alerts_client/alerts_client_api.md new file mode 100644 index 00000000000000..b94a19f8e3f388 --- /dev/null +++ b/x-pack/plugins/rule_registry/docs/alerts_client/alerts_client_api.md @@ -0,0 +1,14 @@ +Alerts as data client API Interface + +# Alerts as data client API Interface + +## Table of contents + +### Classes + +- [AlertsClient](classes/alertsclient.md) + +### Interfaces + +- [ConstructorOptions](interfaces/constructoroptions.md) +- [UpdateOptions](interfaces/updateoptions.md) diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md b/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md new file mode 100644 index 00000000000000..9b639829a9f5fa --- /dev/null +++ b/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md @@ -0,0 +1,191 @@ +[Alerts as data client API Interface](../alerts_client_api.md) / AlertsClient + +# Class: AlertsClient + +Provides apis to interact with alerts as data +ensures the request is authorized to perform read / write actions +on alerts as data. + +## Table of contents + +### Constructors + +- [constructor](alertsclient.md#constructor) + +### Properties + +- [auditLogger](alertsclient.md#auditlogger) +- [authorization](alertsclient.md#authorization) +- [esClient](alertsclient.md#esclient) +- [logger](alertsclient.md#logger) + +### Methods + +- [fetchAlert](alertsclient.md#fetchalert) +- [get](alertsclient.md#get) +- [getAlertsIndex](alertsclient.md#getalertsindex) +- [getAuthorizedAlertsIndices](alertsclient.md#getauthorizedalertsindices) +- [update](alertsclient.md#update) + +## Constructors + +### constructor + +• **new AlertsClient**(`__namedParameters`) + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `__namedParameters` | [ConstructorOptions](../interfaces/constructoroptions.md) | + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:59](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L59) + +## Properties + +### auditLogger + +• `Private` `Optional` `Readonly` **auditLogger**: `AuditLogger` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:57](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L57) + +___ + +### authorization + +• `Private` `Readonly` **authorization**: `PublicMethodsOf` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:58](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L58) + +___ + +### esClient + +• `Private` `Readonly` **esClient**: `ElasticsearchClient` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:59](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L59) + +___ + +### logger + +• `Private` `Readonly` **logger**: `Logger` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:56](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L56) + +## Methods + +### fetchAlert + +▸ `Private` **fetchAlert**(`__namedParameters`): `Promise` + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `__namedParameters` | `GetAlertParams` | + +#### Returns + +`Promise` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:79](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L79) + +___ + +### get + +▸ **get**(`__namedParameters`): `Promise`\>\> + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `__namedParameters` | `GetAlertParams` | + +#### Returns + +`Promise`\>\> + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:108](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L108) + +___ + +### getAlertsIndex + +▸ **getAlertsIndex**(`featureIds`, `operations`): `Promise`<`Object`\> + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `featureIds` | `string`[] | +| `operations` | (`ReadOperations` \| `WriteOperations`)[] | + +#### Returns + +`Promise`<`Object`\> + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:68](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L68) + +___ + +### getAuthorizedAlertsIndices + +▸ **getAuthorizedAlertsIndices**(`featureIds`): `Promise` + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `featureIds` | `string`[] | + +#### Returns + +`Promise` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:200](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L200) + +___ + +### update + +▸ **update**(`__namedParameters`): `Promise`<`Object`\> + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `Params` | `Params`: `AlertTypeParams` = `never` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `__namedParameters` | [UpdateOptions](../interfaces/updateoptions.md) | + +#### Returns + +`Promise`<`Object`\> + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:146](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L146) diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md new file mode 100644 index 00000000000000..e3dbc6b2c2354a --- /dev/null +++ b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md @@ -0,0 +1,52 @@ +[Alerts as data client API Interface](../alerts_client_api.md) / ConstructorOptions + +# Interface: ConstructorOptions + +## Table of contents + +### Properties + +- [auditLogger](constructoroptions.md#auditlogger) +- [authorization](constructoroptions.md#authorization) +- [esClient](constructoroptions.md#esclient) +- [logger](constructoroptions.md#logger) + +## Properties + +### auditLogger + +• `Optional` **auditLogger**: `AuditLogger` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:34](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L34) + +___ + +### authorization + +• **authorization**: `PublicMethodsOf` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:33](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L33) + +___ + +### esClient + +• **esClient**: `ElasticsearchClient` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:35](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L35) + +___ + +### logger + +• **logger**: `Logger` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:32](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L32) diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md new file mode 100644 index 00000000000000..fbc09916350008 --- /dev/null +++ b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md @@ -0,0 +1,58 @@ +[Alerts as data client API Interface](../alerts_client_api.md) / UpdateOptions + +# Interface: UpdateOptions + +## Type parameters + +| Name | Type | +| :------ | :------ | +| `Params` | `Params`: `AlertTypeParams` | + +## Table of contents + +### Properties + +- [\_version](updateoptions.md#_version) +- [id](updateoptions.md#id) +- [index](updateoptions.md#index) +- [status](updateoptions.md#status) + +## Properties + +### \_version + +• **\_version**: `undefined` \| `string` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:41](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L41) + +___ + +### id + +• **id**: `string` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:39](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L39) + +___ + +### index + +• **index**: `string` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:42](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L42) + +___ + +### status + +• **status**: `string` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:40](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L40) diff --git a/x-pack/plugins/rule_registry/docs/alerts_client_typedoc.json b/x-pack/plugins/rule_registry/docs/alerts_client_typedoc.json new file mode 100644 index 00000000000000..5f117323eeb1c3 --- /dev/null +++ b/x-pack/plugins/rule_registry/docs/alerts_client_typedoc.json @@ -0,0 +1,17 @@ +{ + "entryPoints": [ + "../server/alert_data_client/alerts_client.ts" + ], + "exclude": [ + "**/mock.ts", + "../server/alert_data_client/+(mock.ts|utils.ts|utils.test.ts|types.ts)" + ], + "excludeExternals": true, + "out": "alerts_client", + "theme": "markdown", + "plugin": "typedoc-plugin-markdown", + "entryDocument": "alerts_client_api.md", + "readme": "none", + "name": "Alerts as data client API Interface" +} + diff --git a/x-pack/plugins/rule_registry/kibana.json b/x-pack/plugins/rule_registry/kibana.json index 8c1e8d0f5e40ed..f74bebf585eddc 100644 --- a/x-pack/plugins/rule_registry/kibana.json +++ b/x-pack/plugins/rule_registry/kibana.json @@ -12,5 +12,6 @@ "spaces", "triggersActionsUi" ], + "optionalPlugins": ["security"], "server": true } diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts new file mode 100644 index 00000000000000..73c6b4dd405269 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PublicMethodsOf } from '@kbn/utility-types'; +import { AlertsClient } from './alerts_client'; + +type Schema = PublicMethodsOf; +export type AlertsClientMock = jest.Mocked; + +const createAlertsClientMock = () => { + const mocked: AlertsClientMock = { + get: jest.fn(), + getAlertsIndex: jest.fn(), + update: jest.fn(), + getAuthorizedAlertsIndices: jest.fn(), + }; + return mocked; +}; + +export const alertsClientMock: { + create: () => AlertsClientMock; +} = { + create: createAlertsClientMock, +}; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts new file mode 100644 index 00000000000000..553c5ce4472a6c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -0,0 +1,243 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { PublicMethodsOf } from '@kbn/utility-types'; +import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils'; +import { AlertTypeParams } from '../../../alerting/server'; +import { + ReadOperations, + AlertingAuthorization, + WriteOperations, + AlertingAuthorizationEntity, +} from '../../../alerting/server'; +import { Logger, ElasticsearchClient } from '../../../../../src/core/server'; +import { alertAuditEvent, AlertAuditAction } from './audit_events'; +import { AuditLogger } from '../../../security/server'; +import { ALERT_STATUS, OWNER, RULE_ID } from '../../common/technical_rule_data_field_names'; +import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; +import { mapConsumerToIndexName, validFeatureIds, isValidFeatureId } from '../utils/rbac'; + +// TODO: Fix typings https://github.com/elastic/kibana/issues/101776 +type NonNullableProps = Omit & + { [K in Props]-?: NonNullable }; +type AlertType = NonNullableProps; + +const isValidAlert = (source?: ParsedTechnicalFields): source is AlertType => { + return source?.[RULE_ID] != null && source?.[OWNER] != null; +}; +export interface ConstructorOptions { + logger: Logger; + authorization: PublicMethodsOf; + auditLogger?: AuditLogger; + esClient: ElasticsearchClient; +} + +export interface UpdateOptions { + id: string; + status: string; + _version: string | undefined; + index: string; +} + +interface GetAlertParams { + id: string; + index?: string; +} + +/** + * Provides apis to interact with alerts as data + * ensures the request is authorized to perform read / write actions + * on alerts as data. + */ +export class AlertsClient { + private readonly logger: Logger; + private readonly auditLogger?: AuditLogger; + private readonly authorization: PublicMethodsOf; + private readonly esClient: ElasticsearchClient; + + constructor({ auditLogger, authorization, logger, esClient }: ConstructorOptions) { + this.logger = logger; + this.authorization = authorization; + this.esClient = esClient; + this.auditLogger = auditLogger; + } + + public async getAlertsIndex( + featureIds: string[], + operations: Array + ) { + return this.authorization.getAugmentedRuleTypesWithAuthorization( + featureIds.length !== 0 ? featureIds : validFeatureIds, + operations, + AlertingAuthorizationEntity.Alert + ); + } + + private async fetchAlert({ + id, + index, + }: GetAlertParams): Promise<(AlertType & { _version: string | undefined }) | null | undefined> { + try { + const result = await this.esClient.search({ + // Context: Originally thought of always just searching `.alerts-*` but that could + // result in a big performance hit. If the client already knows which index the alert + // belongs to, passing in the index will speed things up + index: index ?? '.alerts-*', + ignore_unavailable: true, + body: { query: { term: { _id: id } } }, + seq_no_primary_term: true, + }); + + if (result == null || result.body == null || result.body.hits.hits.length === 0) { + return; + } + + if (!isValidAlert(result.body.hits.hits[0]._source)) { + const errorMessage = `Unable to retrieve alert details for alert with id of "${id}".`; + this.logger.debug(errorMessage); + throw new Error(errorMessage); + } + + return { + ...result.body.hits.hits[0]._source, + _version: encodeHitVersion(result.body.hits.hits[0]), + }; + } catch (error) { + const errorMessage = `Unable to retrieve alert with id of "${id}".`; + this.logger.debug(errorMessage); + throw error; + } + } + + public async get({ + id, + index, + }: GetAlertParams): Promise { + try { + // first search for the alert by id, then use the alert info to check if user has access to it + const alert = await this.fetchAlert({ + id, + index, + }); + + if (alert == null) { + return; + } + + // this.authorization leverages the alerting plugin's authorization + // client exposed to us for reuse + await this.authorization.ensureAuthorized({ + ruleTypeId: alert[RULE_ID], + consumer: alert[OWNER], + operation: ReadOperations.Get, + entity: AlertingAuthorizationEntity.Alert, + }); + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.GET, + id, + }) + ); + + return alert; + } catch (error) { + this.logger.debug(`Error fetching alert with id of "${id}"`); + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.GET, + id, + error, + }) + ); + throw error; + } + } + + public async update({ + id, + status, + _version, + index, + }: UpdateOptions) { + try { + const alert = await this.fetchAlert({ + id, + index, + }); + + if (alert == null) { + return; + } + + await this.authorization.ensureAuthorized({ + ruleTypeId: alert[RULE_ID], + consumer: alert[OWNER], + operation: WriteOperations.Update, + entity: AlertingAuthorizationEntity.Alert, + }); + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.UPDATE, + id, + outcome: 'unknown', + }) + ); + + const { body: response } = await this.esClient.update({ + ...decodeVersion(_version), + id, + index, + body: { + doc: { + [ALERT_STATUS]: status, + }, + }, + refresh: 'wait_for', + }); + + return { + ...response, + _version: encodeHitVersion(response), + }; + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.UPDATE, + id, + error, + }) + ); + throw error; + } + } + + public async getAuthorizedAlertsIndices(featureIds: string[]): Promise { + const augmentedRuleTypes = await this.authorization.getAugmentedRuleTypesWithAuthorization( + featureIds, + [ReadOperations.Find, ReadOperations.Get, WriteOperations.Update], + AlertingAuthorizationEntity.Alert + ); + + // As long as the user can read a minimum of one type of rule type produced by the provided feature, + // the user should be provided that features' alerts index. + // Limiting which alerts that user can read on that index will be done via the findAuthorizationFilter + const authorizedFeatures = new Set(); + for (const ruleType of augmentedRuleTypes.authorizedRuleTypes) { + authorizedFeatures.add(ruleType.producer); + } + + const toReturn = Array.from(authorizedFeatures).flatMap((feature) => { + if (isValidFeatureId(feature)) { + return mapConsumerToIndexName[feature]; + } + return []; + }); + + return toReturn; + } +} diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts new file mode 100644 index 00000000000000..9e1941f7797220 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Request } from '@hapi/hapi'; + +import { AlertsClientFactory, AlertsClientFactoryProps } from './alerts_client_factory'; +import { ElasticsearchClient, KibanaRequest } from 'src/core/server'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { securityMock } from '../../../security/server/mocks'; +import { AuditLogger } from '../../../security/server'; +import { alertingAuthorizationMock } from '../../../alerting/server/authorization/alerting_authorization.mock'; + +jest.mock('./alerts_client'); + +const securityPluginSetup = securityMock.createSetup(); +const alertingAuthMock = alertingAuthorizationMock.create(); + +const alertsClientFactoryParams: AlertsClientFactoryProps = { + logger: loggingSystemMock.create().get(), + getAlertingAuthorization: (_: KibanaRequest) => alertingAuthMock, + securityPluginSetup, + esClient: {} as ElasticsearchClient, +}; + +const fakeRequest = ({ + app: {}, + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, +} as unknown) as Request; + +const auditLogger = { + log: jest.fn(), +} as jest.Mocked; + +describe('AlertsClientFactory', () => { + beforeEach(() => { + jest.resetAllMocks(); + + securityPluginSetup.audit.asScoped.mockReturnValue(auditLogger); + }); + + test('creates an alerts client with proper constructor arguments', async () => { + const factory = new AlertsClientFactory(); + factory.initialize({ ...alertsClientFactoryParams }); + const request = KibanaRequest.from(fakeRequest); + await factory.create(request); + + expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ + authorization: alertingAuthMock, + logger: alertsClientFactoryParams.logger, + auditLogger, + esClient: {}, + }); + }); + + test('throws an error if already initialized', () => { + const factory = new AlertsClientFactory(); + factory.initialize({ ...alertsClientFactoryParams }); + + expect(() => + factory.initialize({ ...alertsClientFactoryParams }) + ).toThrowErrorMatchingInlineSnapshot(`"AlertsClientFactory (RAC) already initialized"`); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts new file mode 100644 index 00000000000000..43a3827b28972b --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient, KibanaRequest, Logger } from 'src/core/server'; +import { PublicMethodsOf } from '@kbn/utility-types'; +import { SecurityPluginSetup } from '../../../security/server'; +import { AlertingAuthorization } from '../../../alerting/server'; +import { AlertsClient } from './alerts_client'; + +export interface AlertsClientFactoryProps { + logger: Logger; + esClient: ElasticsearchClient; + getAlertingAuthorization: (request: KibanaRequest) => PublicMethodsOf; + securityPluginSetup: SecurityPluginSetup | undefined; +} + +export class AlertsClientFactory { + private isInitialized = false; + private logger!: Logger; + private esClient!: ElasticsearchClient; + private getAlertingAuthorization!: ( + request: KibanaRequest + ) => PublicMethodsOf; + private securityPluginSetup!: SecurityPluginSetup | undefined; + + public initialize(options: AlertsClientFactoryProps) { + /** + * This should be called by the plugin's start() method. + */ + if (this.isInitialized) { + throw new Error('AlertsClientFactory (RAC) already initialized'); + } + + this.getAlertingAuthorization = options.getAlertingAuthorization; + this.isInitialized = true; + this.logger = options.logger; + this.esClient = options.esClient; + this.securityPluginSetup = options.securityPluginSetup; + } + + public async create(request: KibanaRequest): Promise { + const { securityPluginSetup, getAlertingAuthorization, logger } = this; + + return new AlertsClient({ + logger, + authorization: getAlertingAuthorization(request), + auditLogger: securityPluginSetup?.audit.asScoped(request), + esClient: this.esClient, + }); + } +} diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.test.ts new file mode 100644 index 00000000000000..9536a9a640a00c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AlertAuditAction, alertAuditEvent } from './audit_events'; + +describe('#alertAuditEvent', () => { + test('creates event with `unknown` outcome', () => { + expect( + alertAuditEvent({ + action: AlertAuditAction.GET, + outcome: 'unknown', + id: '123', + }) + ).toMatchInlineSnapshot(` + Object { + "error": undefined, + "event": Object { + "action": "alert_get", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "access", + ], + }, + "message": "User is accessing alert [id=123]", + } + `); + }); + + test('creates event with `success` outcome', () => { + expect( + alertAuditEvent({ + action: AlertAuditAction.GET, + id: '123', + }) + ).toMatchInlineSnapshot(` + Object { + "error": undefined, + "event": Object { + "action": "alert_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed alert [id=123]", + } + `); + }); + + test('creates event with `failure` outcome', () => { + expect( + alertAuditEvent({ + action: AlertAuditAction.GET, + id: '123', + error: new Error('ERROR_MESSAGE'), + }) + ).toMatchInlineSnapshot(` + Object { + "error": Object { + "code": "Error", + "message": "ERROR_MESSAGE", + }, + "event": Object { + "action": "alert_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access alert [id=123]", + } + `); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.ts b/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.ts new file mode 100644 index 00000000000000..d07c23c7fbe9fc --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EcsEventOutcome, EcsEventType } from 'src/core/server'; +import { AuditEvent } from '../../../security/server'; + +export enum AlertAuditAction { + GET = 'alert_get', + UPDATE = 'alert_update', + FIND = 'alert_find', +} + +type VerbsTuple = [string, string, string]; + +const eventVerbs: Record = { + alert_get: ['access', 'accessing', 'accessed'], + alert_update: ['update', 'updating', 'updated'], + alert_find: ['access', 'accessing', 'accessed'], +}; + +const eventTypes: Record = { + alert_get: 'access', + alert_update: 'change', + alert_find: 'access', +}; + +export interface AlertAuditEventParams { + action: AlertAuditAction; + outcome?: EcsEventOutcome; + id?: string; + error?: Error; +} + +export function alertAuditEvent({ action, id, outcome, error }: AlertAuditEventParams): AuditEvent { + const doc = id ? `alert [id=${id}]` : 'an alert'; + const [present, progressive, past] = eventVerbs[action]; + const message = error + ? `Failed attempt to ${present} ${doc}` + : outcome === 'unknown' + ? `User is ${progressive} ${doc}` + : `User has ${past} ${doc}`; + const type = eventTypes[action]; + + return { + message, + event: { + action, + category: ['database'], + type: type ? [type] : undefined, + outcome: outcome ?? (error ? 'failure' : 'success'), + }, + error: error && { + code: error.name, + message: error.message, + }, + }; +} diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts new file mode 100644 index 00000000000000..897c17a82b9821 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts @@ -0,0 +1,240 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; +import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock'; +import { AuditLogger } from '../../../../security/server'; + +const alertingAuthMock = alertingAuthorizationMock.create(); +const esClientMock = elasticsearchClientMock.createElasticsearchClient(); +const auditLogger = { + log: jest.fn(), +} as jest.Mocked; + +const alertsClientParams: jest.Mocked = { + logger: loggingSystemMock.create().get(), + authorization: alertingAuthMock, + esClient: esClientMock, + auditLogger, +}; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('get()', () => { + test('calls ES client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + ], + }, + }, + }) + ); + const result = await alertsClient.get({ id: '1', index: '.alerts-observability-apm' }); + expect(result).toMatchInlineSnapshot(` + Object { + "_version": "WzM2MiwyXQ==", + "kibana.rac.alert.owner": "apm", + "kibana.rac.alert.status": "open", + "message": "hello world 1", + "rule.id": "apm.error_rate", + } + `); + expect(esClientMock.search).toHaveBeenCalledTimes(1); + expect(esClientMock.search.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "term": Object { + "_id": "1", + }, + }, + }, + "ignore_unavailable": true, + "index": ".alerts-observability-apm", + "seq_no_primary_term": true, + }, + ] + `); + }); + + test('logs successful event in audit logger', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + ], + }, + }, + }) + ); + await alertsClient.get({ id: '1', index: '.alerts-observability-apm' }); + + expect(auditLogger.log).toHaveBeenCalledWith({ + error: undefined, + event: { action: 'alert_get', category: ['database'], outcome: 'success', type: ['access'] }, + message: 'User has accessed alert [id=1]', + }); + }); + + test(`throws an error if ES client get fails`, async () => { + const error = new Error('something went wrong'); + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockRejectedValue(error); + + await expect( + alertsClient.get({ id: '1', index: '.alerts-observability-apm' }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong"`); + expect(auditLogger.log).toHaveBeenCalledWith({ + error: { code: 'Error', message: 'something went wrong' }, + event: { action: 'alert_get', category: ['database'], outcome: 'failure', type: ['access'] }, + message: 'Failed attempt to access alert [id=1]', + }); + }); + + describe('authorization', () => { + beforeEach(() => { + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + ], + }, + }, + }) + ); + }); + + test('returns alert if user is authorized to read alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + const result = await alertsClient.get({ id: '1', index: '.alerts-observability-apm' }); + + expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'alert', + consumer: 'apm', + operation: 'get', + ruleTypeId: 'apm.error_rate', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "_version": "WzM2MiwyXQ==", + "kibana.rac.alert.owner": "apm", + "kibana.rac.alert.status": "open", + "message": "hello world 1", + "rule.id": "apm.error_rate", + } + `); + }); + + test('throws when user is not authorized to get this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + alertingAuthMock.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get a "apm.error_rate" alert for "apm"`) + ); + + await expect( + alertsClient.get({ id: '1', index: '.alerts-observability-apm' }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "apm.error_rate" alert for "apm"]` + ); + + expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'alert', + consumer: 'apm', + operation: 'get', + ruleTypeId: 'apm.error_rate', + }); + }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts new file mode 100644 index 00000000000000..6fc387fe54b3b4 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts @@ -0,0 +1,376 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; +import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock'; +import { AuditLogger } from '../../../../security/server'; + +const alertingAuthMock = alertingAuthorizationMock.create(); +const esClientMock = elasticsearchClientMock.createElasticsearchClient(); +const auditLogger = { + log: jest.fn(), +} as jest.Mocked; + +const alertsClientParams: jest.Mocked = { + logger: loggingSystemMock.create().get(), + authorization: alertingAuthMock, + esClient: esClientMock, + auditLogger, +}; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('update()', () => { + test('calls ES client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + ], + }, + }, + }) + ); + esClientMock.update.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 2, + result: 'updated', + _shards: { total: 2, successful: 1, failed: 0 }, + _seq_no: 1, + _primary_term: 1, + }, + }) + ); + const result = await alertsClient.update({ + id: '1', + status: 'closed', + _version: undefined, + index: '.alerts-observability-apm', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "_id": "NoxgpHkBqbdrfX07MqXV", + "_index": ".alerts-observability-apm", + "_primary_term": 1, + "_seq_no": 1, + "_shards": Object { + "failed": 0, + "successful": 1, + "total": 2, + }, + "_version": "WzEsMV0=", + "result": "updated", + } + `); + expect(esClientMock.update).toHaveBeenCalledTimes(1); + expect(esClientMock.update.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "doc": Object { + "kibana.rac.alert.status": "closed", + }, + }, + "id": "1", + "index": ".alerts-observability-apm", + "refresh": "wait_for", + }, + ] + `); + }); + + test('logs successful event in audit logger', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + ], + }, + }, + }) + ); + esClientMock.update.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 2, + result: 'updated', + _shards: { total: 2, successful: 1, failed: 0 }, + _seq_no: 1, + _primary_term: 1, + }, + }) + ); + await alertsClient.update({ + id: '1', + status: 'closed', + _version: undefined, + index: '.alerts-observability-apm', + }); + + expect(auditLogger.log).toHaveBeenCalledWith({ + error: undefined, + event: { + action: 'alert_update', + category: ['database'], + outcome: 'unknown', + type: ['change'], + }, + message: 'User is updating alert [id=1]', + }); + }); + + test(`throws an error if ES client get fails`, async () => { + const error = new Error('something went wrong on get'); + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockRejectedValue(error); + + await expect( + alertsClient.update({ + id: '1', + status: 'closed', + _version: undefined, + index: '.alerts-observability-apm', + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong on get"`); + expect(auditLogger.log).toHaveBeenCalledWith({ + error: { code: 'Error', message: 'something went wrong on get' }, + event: { + action: 'alert_update', + category: ['database'], + outcome: 'failure', + type: ['change'], + }, + message: 'Failed attempt to update alert [id=1]', + }); + }); + + test(`throws an error if ES client update fails`, async () => { + const error = new Error('something went wrong on update'); + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + ], + }, + }, + }) + ); + esClientMock.update.mockRejectedValue(error); + + await expect( + alertsClient.update({ + id: '1', + status: 'closed', + _version: undefined, + index: '.alerts-observability-apm', + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong on update"`); + expect(auditLogger.log).toHaveBeenCalledWith({ + error: { code: 'Error', message: 'something went wrong on update' }, + event: { + action: 'alert_update', + category: ['database'], + outcome: 'failure', + type: ['change'], + }, + message: 'Failed attempt to update alert [id=1]', + }); + }); + + describe('authorization', () => { + beforeEach(() => { + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 2, + _seq_no: 362, + _primary_term: 2, + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + ], + }, + }, + }) + ); + + esClientMock.update.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 2, + result: 'updated', + _shards: { total: 2, successful: 1, failed: 0 }, + _seq_no: 1, + _primary_term: 1, + }, + }) + ); + }); + + test('returns alert if user is authorized to update alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + const result = await alertsClient.update({ + id: '1', + status: 'closed', + _version: undefined, + index: '.alerts-observability-apm', + }); + + expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'alert', + consumer: 'apm', + operation: 'update', + ruleTypeId: 'apm.error_rate', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "_id": "NoxgpHkBqbdrfX07MqXV", + "_index": ".alerts-observability-apm", + "_primary_term": 1, + "_seq_no": 1, + "_shards": Object { + "failed": 0, + "successful": 1, + "total": 2, + }, + "_version": "WzEsMV0=", + "result": "updated", + } + `); + }); + + test('throws when user is not authorized to update this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + alertingAuthMock.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get a "apm.error_rate" alert for "apm"`) + ); + + await expect( + alertsClient.update({ + id: '1', + status: 'closed', + _version: undefined, + index: '.alerts-observability-apm', + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "apm.error_rate" alert for "apm"]` + ); + + expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'alert', + consumer: 'apm', + operation: 'update', + ruleTypeId: 'apm.error_rate', + }); + }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_writer.ts b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_writer.ts index 7f83421ec80d85..6fd1c954d8c14c 100644 --- a/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_writer.ts +++ b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_writer.ts @@ -72,7 +72,7 @@ export class IndexWriter { for (const item of items) { if (item.doc === undefined) continue; - bulkBody.push({ create: { _index: item.index } }); + bulkBody.push({ create: { _index: item.index, version: 1 } }); bulkBody.push(item.doc); } diff --git a/x-pack/plugins/rule_registry/server/index.ts b/x-pack/plugins/rule_registry/server/index.ts index 9eefc19f34670e..b6fd6b9a605c0a 100644 --- a/x-pack/plugins/rule_registry/server/index.ts +++ b/x-pack/plugins/rule_registry/server/index.ts @@ -10,6 +10,7 @@ import { RuleRegistryPlugin } from './plugin'; export * from './config'; export type { RuleRegistryPluginSetupContract, RuleRegistryPluginStartContract } from './plugin'; +export type { RacRequestHandlerContext, RacApiRequestHandlerContext } from './types'; export { RuleDataClient } from './rule_data_client'; export { IRuleDataClient } from './rule_data_client/types'; export { getRuleExecutorData, RuleExecutorData } from './utils/get_rule_executor_data'; diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index 043b07f9d67c12..ca98254037732d 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -4,19 +4,34 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { PluginInitializerContext, Plugin, CoreSetup, Logger } from 'src/core/server'; +import { + PluginInitializerContext, + Plugin, + CoreSetup, + Logger, + KibanaRequest, + CoreStart, + IContextProvider, +} from 'src/core/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { AlertsClientFactory } from './alert_data_client/alerts_client_factory'; +import { PluginStartContract as AlertingStart } from '../../alerting/server'; +import { RacApiRequestHandlerContext, RacRequestHandlerContext } from './types'; +import { defineRoutes } from './routes'; import { SpacesPluginStart } from '../../spaces/server'; import { RuleRegistryPluginConfig } from './config'; import { RuleDataPluginService } from './rule_data_plugin_service'; import { EventLogService, IEventLogService } from './event_log'; +import { AlertsClient } from './alert_data_client/alerts_client'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface RuleRegistryPluginSetupDependencies {} +export interface RuleRegistryPluginSetupDependencies { + security?: SecurityPluginSetup; +} -interface RuleRegistryPluginStartDependencies { +export interface RuleRegistryPluginStartDependencies { spaces: SpacesPluginStart; + alerting: AlertingStart; } export interface RuleRegistryPluginSetupContract { @@ -24,7 +39,10 @@ export interface RuleRegistryPluginSetupContract { eventLogService: IEventLogService; } -export type RuleRegistryPluginStartContract = void; +export interface RuleRegistryPluginStartContract { + getRacClientWithRequest: (req: KibanaRequest) => Promise; + alerting: AlertingStart; +} export class RuleRegistryPlugin implements @@ -37,17 +55,23 @@ export class RuleRegistryPlugin private readonly config: RuleRegistryPluginConfig; private readonly logger: Logger; private eventLogService: EventLogService | null; + private readonly alertsClientFactory: AlertsClientFactory; + private ruleDataService: RuleDataPluginService | null; + private security: SecurityPluginSetup | undefined; constructor(initContext: PluginInitializerContext) { this.config = initContext.config.get(); this.logger = initContext.logger.get(); this.eventLogService = null; + this.ruleDataService = null; + this.alertsClientFactory = new AlertsClientFactory(); } public setup( - core: CoreSetup + core: CoreSetup, + plugins: RuleRegistryPluginSetupDependencies ): RuleRegistryPluginSetupContract { - const { config, logger } = this; + const { logger } = this; const startDependencies = core.getStartServices().then(([coreStart, pluginStart]) => { return { @@ -56,23 +80,36 @@ export class RuleRegistryPlugin }; }); - const ruleDataService = new RuleDataPluginService({ - logger, - isWriteEnabled: config.write.enabled, - index: config.index, + this.security = plugins.security; + + const service = new RuleDataPluginService({ + logger: this.logger, + isWriteEnabled: this.config.write.enabled, + index: this.config.index, getClusterClient: async () => { const deps = await startDependencies; return deps.core.elasticsearch.client.asInternalUser; }, }); - ruleDataService.init().catch((originalError) => { + service.init().catch((originalError) => { const error = new Error('Failed installing assets'); // @ts-ignore error.stack = originalError.stack; - logger.error(error); + this.logger.error(error); }); + this.ruleDataService = service; + + // ALERTS ROUTES + const router = core.http.createRouter(); + core.http.registerRouteHandlerContext( + 'rac', + this.createRouteHandlerContext() + ); + + defineRoutes(router); + const eventLogService = new EventLogService({ config: { indexPrefix: this.config.index, @@ -86,10 +123,47 @@ export class RuleRegistryPlugin }); this.eventLogService = eventLogService; - return { ruleDataService, eventLogService }; + + return { ruleDataService: this.ruleDataService, eventLogService }; } - public start(): RuleRegistryPluginStartContract {} + public start( + core: CoreStart, + plugins: RuleRegistryPluginStartDependencies + ): RuleRegistryPluginStartContract { + const { logger, alertsClientFactory, security } = this; + + alertsClientFactory.initialize({ + logger, + esClient: core.elasticsearch.client.asInternalUser, + // NOTE: Alerts share the authorization client with the alerting plugin + getAlertingAuthorization(request: KibanaRequest) { + return plugins.alerting.getAlertingAuthorizationWithRequest(request); + }, + securityPluginSetup: security, + }); + + const getRacClientWithRequest = (request: KibanaRequest) => { + return alertsClientFactory.create(request); + }; + + return { + getRacClientWithRequest, + alerting: plugins.alerting, + }; + } + + private createRouteHandlerContext = (): IContextProvider => { + const { alertsClientFactory } = this; + return function alertsRouteHandlerContext(context, request): RacApiRequestHandlerContext { + return { + getAlertsClient: async () => { + const createdClient = alertsClientFactory.create(request); + return createdClient; + }, + }; + }; + }; public stop() { const { eventLogService, logger } = this; diff --git a/x-pack/plugins/rule_registry/server/routes/__mocks__/request_context.ts b/x-pack/plugins/rule_registry/server/routes/__mocks__/request_context.ts new file mode 100644 index 00000000000000..6d47882ca86c41 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/__mocks__/request_context.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { coreMock, elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; +import { alertsClientMock } from '../../alert_data_client/alerts_client.mock'; +import { RacRequestHandlerContext } from '../../types'; + +const createMockClients = () => ({ + rac: alertsClientMock.create(), + clusterClient: elasticsearchServiceMock.createLegacyScopedClusterClient(), + newClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClient: savedObjectsClientMock.create(), +}); + +const createRequestContextMock = ( + clients: ReturnType = createMockClients() +) => { + const coreContext = coreMock.createRequestHandlerContext(); + return ({ + rac: { getAlertsClient: jest.fn(() => clients.rac) }, + core: { + ...coreContext, + elasticsearch: { + ...coreContext.elasticsearch, + client: clients.newClusterClient, + legacy: { ...coreContext.elasticsearch.legacy, client: clients.clusterClient }, + }, + savedObjects: { client: clients.savedObjectsClient }, + }, + } as unknown) as RacRequestHandlerContext; +}; + +const createTools = () => { + const clients = createMockClients(); + const context = createRequestContextMock(clients); + + return { clients, context }; +}; + +export const requestContextMock = { + create: createRequestContextMock, + createMockClients, + createTools, +}; diff --git a/x-pack/plugins/rule_registry/server/routes/__mocks__/request_responses.ts b/x-pack/plugins/rule_registry/server/routes/__mocks__/request_responses.ts new file mode 100644 index 00000000000000..228fcf491994fd --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/__mocks__/request_responses.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BASE_RAC_ALERTS_API_PATH } from '../../../common/constants'; +import { requestMock } from './server'; + +export const getReadRequest = () => + requestMock.create({ + method: 'get', + path: BASE_RAC_ALERTS_API_PATH, + query: { id: 'alert-1' }, + }); + +export const getUpdateRequest = () => + requestMock.create({ + method: 'patch', + path: BASE_RAC_ALERTS_API_PATH, + body: { + status: 'closed', + ids: ['alert-1'], + index: '.alerts-observability-apm*', + }, + }); diff --git a/x-pack/plugins/rule_registry/server/routes/__mocks__/response_adapters.ts b/x-pack/plugins/rule_registry/server/routes/__mocks__/response_adapters.ts new file mode 100644 index 00000000000000..7952b33dcf9b10 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/__mocks__/response_adapters.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServerMock } from 'src/core/server/mocks'; + +const responseMock = { + create: httpServerMock.createResponseFactory, +}; + +type ResponseMock = ReturnType; +type Method = keyof ResponseMock; + +type MockCall = any; + +interface ResponseCall { + body: any; + status: number; +} + +/** + * @internal + */ +export interface Response extends ResponseCall { + calls: ResponseCall[]; +} + +const buildResponses = (method: Method, calls: MockCall[]): ResponseCall[] => { + if (!calls.length) return []; + + switch (method) { + case 'ok': + return calls.map(([call]) => ({ status: 200, body: call.body })); + case 'customError': + return calls.map(([call]) => ({ + status: call.statusCode, + body: call.body, + })); + default: + throw new Error(`Encountered unexpected call to response.${method}`); + } +}; + +export const responseAdapter = (response: ResponseMock): Response => { + const methods = Object.keys(response) as Method[]; + const calls = methods + .reduce((responses, method) => { + const methodMock = response[method]; + return [...responses, ...buildResponses(method, methodMock.mock.calls)]; + }, []) + .sort((call, other) => other.status - call.status); + + const [{ body, status }] = calls; + + return { + body, + status, + calls, + }; +}; diff --git a/x-pack/plugins/rule_registry/server/routes/__mocks__/server.ts b/x-pack/plugins/rule_registry/server/routes/__mocks__/server.ts new file mode 100644 index 00000000000000..ade72435c57d94 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/__mocks__/server.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RequestHandler, RouteConfig, KibanaRequest } from 'src/core/server'; +import { httpServerMock, httpServiceMock } from 'src/core/server/mocks'; +import { RacRequestHandlerContext } from '../../types'; +import { requestContextMock } from './request_context'; +import { responseAdapter } from './response_adapters'; + +export const requestMock = { + create: httpServerMock.createKibanaRequest, +}; + +export const responseFactoryMock = { + create: httpServerMock.createResponseFactory, +}; + +interface Route { + config: RouteConfig; + handler: RequestHandler; +} +const getRoute = (routerMock: MockServer['router']): Route => { + const routeCalls = [ + ...routerMock.get.mock.calls, + ...routerMock.post.mock.calls, + ...routerMock.put.mock.calls, + ...routerMock.patch.mock.calls, + ...routerMock.delete.mock.calls, + ]; + + const [route] = routeCalls; + if (!route) { + throw new Error('No route registered!'); + } + + const [config, handler] = route; + return { config, handler }; +}; + +const buildResultMock = () => ({ ok: jest.fn((x) => x), badRequest: jest.fn((x) => x) }); + +class MockServer { + constructor( + public readonly router = httpServiceMock.createRouter(), + private responseMock = responseFactoryMock.create(), + private contextMock = requestContextMock.create(), + private resultMock = buildResultMock() + ) {} + + public validate(request: KibanaRequest) { + this.validateRequest(request); + return this.resultMock; + } + + public async inject( + request: KibanaRequest, + context: RacRequestHandlerContext = this.contextMock + ) { + const validatedRequest = this.validateRequest(request); + const [rejection] = this.resultMock.badRequest.mock.calls; + if (rejection) { + throw new Error(`Request was rejected with message: '${rejection}'`); + } + + await this.getRoute().handler(context, validatedRequest, this.responseMock); + return responseAdapter(this.responseMock); + } + + private getRoute(): Route { + return getRoute(this.router); + } + + private maybeValidate(part: any, validator?: any): any { + return typeof validator === 'function' ? validator(part, this.resultMock) : part; + } + + private validateRequest(request: KibanaRequest): KibanaRequest { + const validations = this.getRoute().config.validate; + if (!validations) { + return request; + } + + const validatedRequest = requestMock.create({ + path: request.route.path, + method: request.route.method, + body: this.maybeValidate(request.body, validations.body), + query: this.maybeValidate(request.query, validations.query), + params: this.maybeValidate(request.params, validations.params), + }); + + return validatedRequest; + } +} + +const createMockServer = () => new MockServer(); + +export const serverMock = { + create: createMockServer, +}; diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts new file mode 100644 index 00000000000000..0de1e6c585a17f --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; +import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; +import { getAlertByIdRoute } from './get_alert_by_id'; +import { requestContextMock } from './__mocks__/request_context'; +import { getReadRequest } from './__mocks__/request_responses'; +import { requestMock, serverMock } from './__mocks__/server'; + +const getMockAlert = (): ParsedTechnicalFields => ({ + '@timestamp': '2021-06-21T21:33:05.713Z', + 'rule.id': 'apm.error_rate', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', +}); + +describe('getAlertByIdRoute', () => { + let server: ReturnType; + let { clients, context } = requestContextMock.createTools(); + + beforeEach(async () => { + server = serverMock.create(); + ({ clients, context } = requestContextMock.createTools()); + + clients.rac.get.mockResolvedValue(getMockAlert()); + + getAlertByIdRoute(server.router); + }); + + test('returns 200 when finding a single alert with valid params', async () => { + const response = await server.inject(getReadRequest(), context); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(getMockAlert()); + }); + + test('returns 200 when finding a single alert with index param', async () => { + const response = await server.inject( + requestMock.create({ + method: 'get', + path: BASE_RAC_ALERTS_API_PATH, + query: { id: 'alert-1', index: '.alerts-me' }, + }), + context + ); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(getMockAlert()); + }); + + describe('request validation', () => { + test('rejects invalid query params', async () => { + await expect( + server.inject( + requestMock.create({ + method: 'get', + path: BASE_RAC_ALERTS_API_PATH, + query: { id: 4 }, + }), + context + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request was rejected with message: 'Invalid value \\"4\\" supplied to \\"id\\"'"` + ); + }); + + test('rejects unknown query params', async () => { + await expect( + server.inject( + requestMock.create({ + method: 'get', + path: BASE_RAC_ALERTS_API_PATH, + query: { notId: 4 }, + }), + context + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request was rejected with message: 'Invalid value \\"undefined\\" supplied to \\"id\\"'"` + ); + }); + }); + + test('returns error status if rac client "GET" fails', async () => { + clients.rac.get.mockRejectedValue(new Error('Unable to get alert')); + const response = await server.inject(getReadRequest(), context); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + attributes: { success: false }, + message: 'Unable to get alert', + }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts new file mode 100644 index 00000000000000..9ddec56055a5a8 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from 'kibana/server'; +import * as t from 'io-ts'; +import { id as _id } from '@kbn/securitysolution-io-ts-list-types'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +import { RacRequestHandlerContext } from '../types'; +import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; +import { buildRouteValidation } from './utils/route_validation'; + +export const getAlertByIdRoute = (router: IRouter) => { + router.get( + { + path: BASE_RAC_ALERTS_API_PATH, + validate: { + query: buildRouteValidation( + t.intersection([ + t.exact( + t.type({ + id: _id, + }) + ), + t.exact( + t.partial({ + index: t.string, + }) + ), + ]) + ), + }, + options: { + tags: ['access:rac'], + }, + }, + async (context, request, response) => { + try { + const alertsClient = await context.rac.getAlertsClient(); + const { id, index } = request.query; + const alert = await alertsClient.get({ id, index }); + if (alert == null) { + return response.notFound({ + body: { message: `alert with id ${id} and index ${index} not found` }, + }); + } + return response.ok({ + body: alert, + }); + } catch (exc) { + const err = transformError(exc); + const contentType = { + 'content-type': 'application/json', + }; + const defaultedHeaders = { + ...contentType, + }; + + return response.customError({ + headers: defaultedHeaders, + statusCode: err.statusCode, + body: { + message: err.message, + attributes: { + success: false, + }, + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts new file mode 100644 index 00000000000000..b8b181a493cec0 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from 'kibana/server'; +import { id as _id } from '@kbn/securitysolution-io-ts-list-types'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +import { RacRequestHandlerContext } from '../types'; +import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; +import { validFeatureIds } from '../utils/rbac'; + +export const getAlertsIndexRoute = (router: IRouter) => { + router.get( + { + path: `${BASE_RAC_ALERTS_API_PATH}/index`, + validate: false, + options: { + tags: ['access:rac'], + }, + }, + async (context, request, response) => { + try { + const alertsClient = await context.rac.getAlertsClient(); + const indexName = await alertsClient.getAuthorizedAlertsIndices(validFeatureIds); + return response.ok({ + body: { index_name: indexName }, + }); + } catch (exc) { + const err = transformError(exc); + const contentType = { + 'content-type': 'application/json', + }; + const defaultedHeaders = { + ...contentType, + }; + + return response.custom({ + headers: defaultedHeaders, + statusCode: err.statusCode, + body: Buffer.from( + JSON.stringify({ + message: err.message, + status_code: err.statusCode, + }) + ), + }); + } + } + ); +}; diff --git a/x-pack/plugins/rule_registry/server/routes/index.ts b/x-pack/plugins/rule_registry/server/routes/index.ts new file mode 100644 index 00000000000000..6698cd7717268d --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from 'kibana/server'; +import { RacRequestHandlerContext } from '../types'; +import { getAlertByIdRoute } from './get_alert_by_id'; +import { updateAlertByIdRoute } from './update_alert_by_id'; +import { getAlertsIndexRoute } from './get_alert_index'; + +export function defineRoutes(router: IRouter) { + getAlertByIdRoute(router); + updateAlertByIdRoute(router); + getAlertsIndexRoute(router); +} diff --git a/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.test.ts b/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.test.ts new file mode 100644 index 00000000000000..7ec699491ca83e --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; +import { updateAlertByIdRoute } from './update_alert_by_id'; +import { requestContextMock } from './__mocks__/request_context'; +import { getUpdateRequest } from './__mocks__/request_responses'; +import { requestMock, serverMock } from './__mocks__/server'; + +describe('updateAlertByIdRoute', () => { + let server: ReturnType; + let { clients, context } = requestContextMock.createTools(); + + beforeEach(async () => { + server = serverMock.create(); + ({ clients, context } = requestContextMock.createTools()); + + clients.rac.update.mockResolvedValue({ + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 'WzM2MiwyXQ==', + result: 'updated', + _shards: { total: 2, successful: 1, failed: 0 }, + _seq_no: 1, + _primary_term: 1, + }); + + updateAlertByIdRoute(server.router); + }); + + test('returns 200 when updating a single alert with valid params', async () => { + const response = await server.inject(getUpdateRequest(), context); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 'WzM2MiwyXQ==', + result: 'updated', + _shards: { total: 2, successful: 1, failed: 0 }, + _seq_no: 1, + _primary_term: 1, + success: true, + }); + }); + + describe('request validation', () => { + test('rejects invalid query params', async () => { + await expect( + server.inject( + requestMock.create({ + method: 'patch', + path: BASE_RAC_ALERTS_API_PATH, + body: { + status: 'closed', + ids: 'alert-1', + index: '.alerts-observability-apm*', + }, + }), + context + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request was rejected with message: 'Invalid value \\"alert-1\\" supplied to \\"ids\\"'"` + ); + }); + + test('rejects unknown query params', async () => { + await expect( + server.inject( + requestMock.create({ + method: 'patch', + path: BASE_RAC_ALERTS_API_PATH, + body: { + notStatus: 'closed', + ids: ['alert-1'], + index: '.alerts-observability-apm*', + }, + }), + context + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request was rejected with message: 'Invalid value \\"undefined\\" supplied to \\"status\\"'"` + ); + }); + }); + + test('returns error status if rac client "GET" fails', async () => { + clients.rac.update.mockRejectedValue(new Error('Unable to update alert')); + const response = await server.inject(getUpdateRequest(), context); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + attributes: { success: false }, + message: 'Unable to update alert', + }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.ts b/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.ts new file mode 100644 index 00000000000000..a77688a514e773 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from 'kibana/server'; +import * as t from 'io-ts'; +import { id as _id } from '@kbn/securitysolution-io-ts-list-types'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +import { buildRouteValidation } from './utils/route_validation'; +import { RacRequestHandlerContext } from '../types'; +import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; + +export const updateAlertByIdRoute = (router: IRouter) => { + router.post( + { + path: BASE_RAC_ALERTS_API_PATH, + validate: { + body: buildRouteValidation( + t.intersection([ + t.exact( + t.type({ + status: t.string, + ids: t.array(t.string), + index: t.string, + }) + ), + t.exact( + t.partial({ + _version: t.string, + }) + ), + ]) + ), + }, + options: { + tags: ['access:rac'], + }, + }, + async (context, req, response) => { + try { + const alertsClient = await context.rac.getAlertsClient(); + const { status, ids, index, _version } = req.body; + + const updatedAlert = await alertsClient.update({ + id: ids[0], + status, + _version, + index, + }); + + if (updatedAlert == null) { + return response.notFound({ + body: { message: `alerts with ids ${ids} and index ${index} not found` }, + }); + } + + return response.ok({ body: { success: true, ...updatedAlert } }); + } catch (exc) { + const err = transformError(exc); + + const contentType = { + 'content-type': 'application/json', + }; + const defaultedHeaders = { + ...contentType, + }; + + return response.customError({ + headers: defaultedHeaders, + statusCode: err.statusCode, + body: { + message: err.message, + attributes: { + success: false, + }, + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/rule_registry/server/routes/utils/route_validation.ts b/x-pack/plugins/rule_registry/server/routes/utils/route_validation.ts new file mode 100644 index 00000000000000..8e74760d6d15f3 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/utils/route_validation.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; +import { exactCheck, formatErrors } from '@kbn/securitysolution-io-ts-utils'; + +import { + RouteValidationError, + RouteValidationFunction, + RouteValidationResultFactory, +} from '../../../../../../src/core/server'; + +type RequestValidationResult = + | { + value: T; + error?: undefined; + } + | { + value?: undefined; + error: RouteValidationError; + }; + +/** + * Copied from x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts + * This really should be in @kbn/securitysolution-io-ts-utils rather than copied yet again, however, this has types + * from a lot of places such as RouteValidationResultFactory from core/server which in turn can pull in @kbn/schema + * which cannot work on the front end and @kbn/securitysolution-io-ts-utils works on both front and backend. + * + * TODO: Figure out a way to move this function into a package rather than copying it/forking it within plugins + */ +export const buildRouteValidation = >( + schema: T +): RouteValidationFunction => ( + inputValue: unknown, + validationResult: RouteValidationResultFactory +): RequestValidationResult => + pipe( + schema.decode(inputValue), + (decoded) => exactCheck(inputValue, decoded), + fold>( + (errors: rt.Errors) => validationResult.badRequest(formatErrors(errors).join()), + (validatedInput: A) => validationResult.ok(validatedInput) + ) + ); diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts index 3b90079ec52389..54e9a1b3c9a6f3 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts @@ -11,6 +11,7 @@ import { ElasticsearchClient } from 'kibana/server'; import { FieldDescriptor } from 'src/plugins/data/server'; import { ESSearchRequest, ESSearchResponse } from 'src/core/types/elasticsearch'; import { TechnicalRuleDataFieldName } from '../../common/technical_rule_data_field_names'; +import { ValidFeatureId } from '../utils/rbac'; export interface RuleDataReader { search( @@ -37,9 +38,17 @@ export interface IRuleDataClient { createWriteTargetIfNeeded(options: { namespace?: string }): Promise; } +/** + * The purpose of the `feature` param is to force the user to update + * the data structure which contains the mapping of consumers to alerts + * as data indices. The idea is it is typed such that it forces the + * user to go to the code and modify it. At least until a better system + * is put in place or we move the alerts as data client out of rule registry. + */ export interface RuleDataClientConstructorOptions { getClusterClient: () => Promise; isWriteEnabled: boolean; ready: () => Promise; alias: string; + feature: ValidFeatureId; } diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts index 33ff5281147e15..d84f85dbc99b77 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts @@ -20,10 +20,11 @@ import { ClusterPutComponentTemplateBody, PutIndexTemplateRequest } from '../../ import { RuleDataClient } from '../rule_data_client'; import { RuleDataWriteDisabledError } from './errors'; import { incrementIndexName } from './utils'; +import { ValidFeatureId } from '../utils/rbac'; const BOOTSTRAP_TIMEOUT = 60000; -interface RuleDataPluginServiceConstructorOptions { +export interface RuleDataPluginServiceConstructorOptions { getClusterClient: () => Promise; logger: Logger; isWriteEnabled: boolean; @@ -223,9 +224,10 @@ export class RuleDataPluginService { return [this.options.index, assetName].filter(Boolean).join('-'); } - getRuleDataClient(alias: string, initialize: () => Promise) { + getRuleDataClient(feature: ValidFeatureId, alias: string, initialize: () => Promise) { return new RuleDataClient({ alias, + feature, getClusterClient: () => this.getClusterClient(), isWriteEnabled: this.isWriteEnabled(), ready: initialize, diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts new file mode 100644 index 00000000000000..275d68621864fc --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PublicMethodsOf } from '@kbn/utility-types'; +import { RuleDataPluginService, RuleDataPluginServiceConstructorOptions } from './'; + +type Schema = PublicMethodsOf; + +const createRuleDataPluginServiceMock = (_: RuleDataPluginServiceConstructorOptions) => { + const mocked: jest.Mocked = { + init: jest.fn(), + isReady: jest.fn(), + wait: jest.fn(), + isWriteEnabled: jest.fn(), + getFullAssetName: jest.fn(), + createOrUpdateComponentTemplate: jest.fn(), + createOrUpdateIndexTemplate: jest.fn(), + createOrUpdateLifecyclePolicy: jest.fn(), + getRuleDataClient: jest.fn(), + updateIndexMappingsMatchingPattern: jest.fn(), + }; + return mocked; +}; + +export const ruleDataPluginServiceMock: { + create: ( + _: RuleDataPluginServiceConstructorOptions + ) => jest.Mocked>; +} = { + create: createRuleDataPluginServiceMock, +}; diff --git a/x-pack/plugins/rule_registry/server/scripts/README.md b/x-pack/plugins/rule_registry/server/scripts/README.md new file mode 100644 index 00000000000000..2b3f01f3c4d6ba --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/README.md @@ -0,0 +1,24 @@ +Users with roles granting them access to monitoring (observability) and siem (security solution) should only be able to access alerts with those roles + +```bash +myterminal~$ ./get_security_solution_alert.sh observer +{ + "statusCode": 404, + "error": "Not Found", + "message": "Unauthorized to get \"rac:8.0.0:securitySolution/get\" alert\"" +} +myterminal~$ ./get_security_solution_alert.sh +{ + "success": true +} +myterminal~$ ./get_observability_alert.sh +{ + "success": true +} +myterminal~$ ./get_observability_alert.sh hunter +{ + "statusCode": 404, + "error": "Not Found", + "message": "Unauthorized to get \"rac:8.0.0:observability/get\" alert\"" +} +``` diff --git a/x-pack/plugins/rule_registry/server/scripts/get_alerts_index.sh b/x-pack/plugins/rule_registry/server/scripts/get_alerts_index.sh new file mode 100755 index 00000000000000..bfa74aa016f02e --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/get_alerts_index.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e + +USER=${1:-'observer'} + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + +# Example: ./find_rules.sh +curl -v -k \ + -u $USER:changeme \ + -X GET "${KIBANA_URL}${SPACE_URL}/internal/rac/alerts/index" | jq . + +# -X GET "${KIBANA_URL}${SPACE_URL}/api/apm/settings/apm-alerts-as-data-indices" | jq . diff --git a/x-pack/plugins/rule_registry/server/scripts/get_observability_alert.sh b/x-pack/plugins/rule_registry/server/scripts/get_observability_alert.sh new file mode 100755 index 00000000000000..6fbd0eb3dc816a --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/get_observability_alert.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e + +USER=${1:-'observer'} +ID=${2:-'DHEnOXoB8br9Z2X1fq_l'} + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + +# Example: ./get_observability_alert.sh hunter +curl -v -k \ + -u $USER:changeme \ + -X GET "${KIBANA_URL}${SPACE_URL}/internal/rac/alerts?id=$ID&index=.alerts-observability-apm" | jq . diff --git a/x-pack/plugins/rule_registry/server/scripts/get_security_alert.sh b/x-pack/plugins/rule_registry/server/scripts/get_security_alert.sh new file mode 100755 index 00000000000000..9bf051c1c6412e --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/get_security_alert.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e + +USER=${1:-'hunter'} +ID=${2:-'kdL4gHoBFALkyfScIsY5'} + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + +# Example: ./get_observability_alert.sh hunter +curl -v -k \ + -u $USER:changeme \ + -X GET "${KIBANA_URL}${SPACE_URL}/internal/rac/alerts?id=$ID&index=.alerts-security-solution" | jq . diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/README.md b/x-pack/plugins/rule_registry/server/scripts/hunter/README.md new file mode 100644 index 00000000000000..a0269d5b060a30 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/README.md @@ -0,0 +1,5 @@ +This user can access the monitoring route at http://localhost:5601/security-myfakepath + +| Role | Data Sources | Security Solution ML Jobs/Results | Lists | Rules/Exceptions | Action Connectors | Signals/Alerts | +| :-----------------: | :----------: | :-------------------------------: | :---: | :--------------: | :---------------: | :------------: | +| Hunter / T3 Analyst | read, write | read | read | read, write | read | read, write | diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/delete_detections_user.sh b/x-pack/plugins/rule_registry/server/scripts/hunter/delete_detections_user.sh new file mode 100755 index 00000000000000..595f0a49282d8c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/delete_detections_user.sh @@ -0,0 +1,11 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +curl -v -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XDELETE ${ELASTICSEARCH_URL}/_security/user/hunter diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/detections_role.json b/x-pack/plugins/rule_registry/server/scripts/hunter/detections_role.json new file mode 100644 index 00000000000000..80f63f80b849c5 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/detections_role.json @@ -0,0 +1,19 @@ +{ + "elasticsearch": { + "cluster": [], + "indices": [] + }, + "kibana": [ + { + "feature": { + "ml": ["read"], + "siem": ["all"], + "actions": ["read"], + "ruleRegistry": ["all"], + "builtInAlerts": ["all"], + "alerting": ["all"] + }, + "spaces": ["*"] + } + ] +} diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/detections_user.json b/x-pack/plugins/rule_registry/server/scripts/hunter/detections_user.json new file mode 100644 index 00000000000000..f9454cc0ad2fe8 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/detections_user.json @@ -0,0 +1,6 @@ +{ + "password": "changeme", + "roles": ["hunter"], + "full_name": "Hunter", + "email": "detections-reader@example.com" +} diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/get_detections_role.sh b/x-pack/plugins/rule_registry/server/scripts/hunter/get_detections_role.sh new file mode 100755 index 00000000000000..7ec850ce220bb3 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/get_detections_role.sh @@ -0,0 +1,11 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XGET ${KIBANA_URL}/api/security/role/hunter | jq -S . diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/index.ts b/x-pack/plugins/rule_registry/server/scripts/hunter/index.ts new file mode 100644 index 00000000000000..3411589de77211 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as hunterUser from './detections_user.json'; +import * as hunterRole from './detections_role.json'; +export { hunterUser, hunterRole }; diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_role.sh b/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_role.sh new file mode 100755 index 00000000000000..debffe0fcac4c4 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_role.sh @@ -0,0 +1,14 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +ROLE=(${@:-./detections_role.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XPUT ${KIBANA_URL}/api/security/role/hunter \ +-d @${ROLE} diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_user.sh b/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_user.sh new file mode 100755 index 00000000000000..ab2a0530813948 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_user.sh @@ -0,0 +1,14 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +USER=(${@:-./detections_user.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + ${ELASTICSEARCH_URL}/_security/user/hunter \ +-d @${USER} diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/README.md b/x-pack/plugins/rule_registry/server/scripts/observer/README.md new file mode 100644 index 00000000000000..dc7e989ba46355 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/README.md @@ -0,0 +1,5 @@ +This user can access the monitoring route at http://localhost:5601/monitoring-myfakepath + +| Role | Data Sources | Security Solution ML Jobs/Results | Lists | Rules/Exceptions | Action Connectors | Signals/Alerts | +| :------: | :----------: | :-------------------------------: | :---: | :--------------: | :---------------: | :------------: | +| observer | read, write | read | read | read, write | read | read, write | \ No newline at end of file diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/delete_detections_user.sh b/x-pack/plugins/rule_registry/server/scripts/observer/delete_detections_user.sh new file mode 100755 index 00000000000000..017d8904a51e10 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/delete_detections_user.sh @@ -0,0 +1,11 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +curl -v -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XDELETE ${ELASTICSEARCH_URL}/_security/user/observer diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/detections_role.json b/x-pack/plugins/rule_registry/server/scripts/observer/detections_role.json new file mode 100644 index 00000000000000..dd3d3f96e3a33b --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/detections_role.json @@ -0,0 +1,20 @@ +{ + "elasticsearch": { + "cluster": [], + "indices": [] + }, + "kibana": [ + { + "feature": { + "ml": ["read"], + "monitoring": ["all"], + "apm": ["minimal_read", "alerts_all"], + "ruleRegistry": ["all"], + "actions": ["read"], + "builtInAlerts": ["all"], + "alerting": ["all"] + }, + "spaces": ["*"] + } + ] +} diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/detections_user.json b/x-pack/plugins/rule_registry/server/scripts/observer/detections_user.json new file mode 100644 index 00000000000000..9f06e7dcc29f18 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/detections_user.json @@ -0,0 +1,6 @@ +{ + "password": "changeme", + "roles": ["observer"], + "full_name": "Observer", + "email": "monitoring-observer@example.com" +} \ No newline at end of file diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/get_detections_role.sh b/x-pack/plugins/rule_registry/server/scripts/observer/get_detections_role.sh new file mode 100755 index 00000000000000..7ec850ce220bb3 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/get_detections_role.sh @@ -0,0 +1,11 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XGET ${KIBANA_URL}/api/security/role/hunter | jq -S . diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/get_observability_alert.sh b/x-pack/plugins/rule_registry/server/scripts/observer/get_observability_alert.sh new file mode 100755 index 00000000000000..dd71e9dc6af43b --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/get_observability_alert.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e + +USER=${1:-'observer'} + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + +# Example: ./find_rules.sh +curl -s -k \ + -u $USER:changeme \ + -X GET ${KIBANA_URL}${SPACE_URL}/monitoring-myfakepath | jq . diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/get_security_solution_alert.sh b/x-pack/plugins/rule_registry/server/scripts/observer/get_security_solution_alert.sh new file mode 100755 index 00000000000000..b4348266c96341 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/get_security_solution_alert.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + + +USER=${1:-'hunter'} + +# Example: ./find_rules.sh +curl -s -k \ + -u $USER:changeme \ + -X GET ${KIBANA_URL}${SPACE_URL}/security-myfakepath | jq . diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/index.ts b/x-pack/plugins/rule_registry/server/scripts/observer/index.ts new file mode 100644 index 00000000000000..5feebc1caeed1c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as observerUser from './detections_user.json'; +import * as observerRole from './detections_role.json'; +export { observerUser, observerRole }; diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_role.sh b/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_role.sh new file mode 100755 index 00000000000000..4dddb64befc6b8 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_role.sh @@ -0,0 +1,14 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +ROLE=(${@:-./detections_role.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XPUT ${KIBANA_URL}/api/security/role/observer \ +-d @${ROLE} \ No newline at end of file diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_user.sh b/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_user.sh new file mode 100755 index 00000000000000..8a897c0d28142f --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_user.sh @@ -0,0 +1,14 @@ + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +USER=(${@:-./detections_user.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + ${ELASTICSEARCH_URL}/_security/user/observer \ +-d @${USER} \ No newline at end of file diff --git a/x-pack/plugins/rule_registry/server/scripts/update_observability_alert.sh b/x-pack/plugins/rule_registry/server/scripts/update_observability_alert.sh new file mode 100755 index 00000000000000..f61fcf2662aa37 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/update_observability_alert.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e + +IDS=${1} +STATUS=${2} + +echo $IDS +echo "'"$STATUS"'" + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + +# Example: ./update_observability_alert.sh [\"my-alert-id\",\"another-alert-id\"] +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u observer:changeme \ + -X POST ${KIBANA_URL}${SPACE_URL}/internal/rac/alerts \ + -d "{\"ids\": $IDS, \"status\":\"$STATUS\", \"index\":\".alerts-observability-apm\"}" | jq . diff --git a/x-pack/plugins/rule_registry/server/types.ts b/x-pack/plugins/rule_registry/server/types.ts index 959c05fd1334e2..f8bd1940b10a8e 100644 --- a/x-pack/plugins/rule_registry/server/types.ts +++ b/x-pack/plugins/rule_registry/server/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { RequestHandlerContext } from 'kibana/server'; import { AlertInstanceContext, AlertInstanceState, @@ -12,6 +13,7 @@ import { AlertTypeState, } from '../../alerting/common'; import { AlertType } from '../../alerting/server'; +import { AlertsClient } from './alert_data_client/alerts_client'; type SimpleAlertType< TParams extends AlertTypeParams = {}, @@ -38,3 +40,17 @@ export type AlertTypeWithExecutor< > & { executor: AlertTypeExecutor; }; + +/** + * @public + */ +export interface RacApiRequestHandlerContext { + getAlertsClient: () => Promise; +} + +/** + * @internal + */ +export interface RacRequestHandlerContext extends RequestHandlerContext { + rac: RacApiRequestHandlerContext; +} diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index 38ddbd3f1876b3..a37ba9ef566366 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -194,6 +194,7 @@ describe('createLifecycleRuleTypeFactory', () => { "event.kind": "event", "kibana.rac.alert.duration.us": 0, "kibana.rac.alert.id": "opbeans-java", + "kibana.rac.alert.owner": "consumer", "kibana.rac.alert.producer": "test", "kibana.rac.alert.start": "2021-06-16T09:01:00.000Z", "kibana.rac.alert.status": "open", @@ -212,6 +213,7 @@ describe('createLifecycleRuleTypeFactory', () => { "event.kind": "event", "kibana.rac.alert.duration.us": 0, "kibana.rac.alert.id": "opbeans-node", + "kibana.rac.alert.owner": "consumer", "kibana.rac.alert.producer": "test", "kibana.rac.alert.start": "2021-06-16T09:01:00.000Z", "kibana.rac.alert.status": "open", @@ -230,6 +232,7 @@ describe('createLifecycleRuleTypeFactory', () => { "event.kind": "signal", "kibana.rac.alert.duration.us": 0, "kibana.rac.alert.id": "opbeans-java", + "kibana.rac.alert.owner": "consumer", "kibana.rac.alert.producer": "test", "kibana.rac.alert.start": "2021-06-16T09:01:00.000Z", "kibana.rac.alert.status": "open", @@ -248,6 +251,7 @@ describe('createLifecycleRuleTypeFactory', () => { "event.kind": "signal", "kibana.rac.alert.duration.us": 0, "kibana.rac.alert.id": "opbeans-node", + "kibana.rac.alert.owner": "consumer", "kibana.rac.alert.producer": "test", "kibana.rac.alert.start": "2021-06-16T09:01:00.000Z", "kibana.rac.alert.status": "open", diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts index 005af59892b8ae..34045a2a905f8f 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts @@ -25,6 +25,7 @@ import { ALERT_UUID, EVENT_ACTION, EVENT_KIND, + OWNER, RULE_UUID, TIMESTAMP, } from '../../common/technical_rule_data_field_names'; @@ -69,6 +70,7 @@ export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({ const { services: { alertInstanceFactory }, state: previousState, + rule, } = options; const ruleExecutorData = getRuleExecutorData(type, options); @@ -180,6 +182,7 @@ export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({ ...ruleExecutorData, [TIMESTAMP]: timestamp, [EVENT_KIND]: 'event', + [OWNER]: rule.consumer, [ALERT_ID]: alertId, }; @@ -234,6 +237,7 @@ export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({ [EVENT_KIND]: 'signal', }); } + logger.debug(`Preparing to index ${eventsToIndex.length} alerts.`); if (ruleDataClient.isWriteEnabled()) { await ruleDataClient.getWriter().bulk({ diff --git a/x-pack/plugins/rule_registry/server/utils/rbac.ts b/x-pack/plugins/rule_registry/server/utils/rbac.ts new file mode 100644 index 00000000000000..812dbb84088123 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/rbac.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * registering a new instance of the rule data client + * in a new plugin will require updating the below data structure + * to include the index name where the alerts as data will be written to. + */ +export const mapConsumerToIndexName = { + apm: '.alerts-observability-apm', + observability: '.alerts-observability', + siem: ['.alerts-security.alerts', '.siem-signals'], +}; +export type ValidFeatureId = keyof typeof mapConsumerToIndexName; + +export const validFeatureIds = Object.keys(mapConsumerToIndexName); +export const isValidFeatureId = (a: unknown): a is ValidFeatureId => + typeof a === 'string' && validFeatureIds.includes(a); diff --git a/x-pack/plugins/rule_registry/tsconfig.json b/x-pack/plugins/rule_registry/tsconfig.json index 5aefe9769da22b..f6253e441da312 100644 --- a/x-pack/plugins/rule_registry/tsconfig.json +++ b/x-pack/plugins/rule_registry/tsconfig.json @@ -7,11 +7,19 @@ "declaration": true, "declarationMap": true }, - "include": ["common/**/*", "server/**/*", "public/**/*", "../../../typings/**/*"], + "include": [ + "common/**/*", + "server/**/*", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "server/**/*.json", + "public/**/*", + "../../../typings/**/*" + ], "references": [ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json" }, { "path": "../alerting/tsconfig.json" }, + { "path": "../security/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, { "path": "../triggers_actions_ui/tsconfig.json" } ] diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 4389b226117481..a8ad6c919a04d0 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -238,6 +238,7 @@ export class Plugin implements IPlugin initializeRuleDataTemplatesPromise ); @@ -338,7 +339,7 @@ export class Plugin implements IPlugin { - before(() => esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts')); - after(() => esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts')); + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + await createSpacesAndUsers(getService); + }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + await deleteSpacesAndUsers(getService); + }); it('Make sure that we get Timeline data', async () => { await retry.try(async () => { @@ -454,6 +468,60 @@ export default function ({ getService }: FtrProviderContext) { }); }); + // TODO: unskip this test once authz is added to search strategy + it.skip('Make sure that we get Timeline data using the hunter role and do not receive observability alerts', async () => { + await retry.try(async () => { + const requestBody = { + defaultIndex: ['.alerts*'], // query both .alerts-observability-apm and .alerts-security-solution + docValueFields: [], + factoryQueryType: TimelineEventsQueries.all, + fieldRequested: FIELD_REQUESTED, + // fields: [], + filterQuery: { + bool: { + filter: [ + { + match_all: {}, + }, + ], + }, + }, + pagination: { + activePage: 0, + querySize: 25, + }, + language: 'kuery', + sort: [ + { + field: '@timestamp', + direction: Direction.desc, + type: 'number', + }, + ], + timerange: { + from: FROM, + to: TO, + interval: '12h', + }, + }; + const resp = await supertestWithoutAuth + .post('/internal/search/securitySolutionTimelineSearchStrategy/') + .auth(secOnly.username, secOnly.password) // using security 'hunter' role + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send(requestBody) + .expect(200); + + const timeline = resp.body; + + // we inject one alert into the security solutions alerts index and another alert into the observability alerts index + // therefore when accessing the .alerts* index with the security solution user, + // only security solution alerts should be returned since the security solution user + // is not authorized to view observability alerts. + expect(timeline.totalCount).to.be(1); + }); + }); + it('Make sure that pagination is working in Timeline query', async () => { await retry.try(async () => { const resp = await supertest diff --git a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts index e07681afe22037..4e3740a1ccb1cb 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts @@ -367,6 +367,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { "kibana.rac.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], + "kibana.rac.alert.owner": Array [ + "apm", + ], "kibana.rac.alert.producer": Array [ "apm", ], @@ -437,6 +440,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { "kibana.rac.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], + "kibana.rac.alert.owner": Array [ + "apm", + ], "kibana.rac.alert.producer": Array [ "apm", ], @@ -541,6 +547,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { "kibana.rac.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], + "kibana.rac.alert.owner": Array [ + "apm", + ], "kibana.rac.alert.producer": Array [ "apm", ], diff --git a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json new file mode 100644 index 00000000000000..a9837210c2e5a1 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json @@ -0,0 +1,29 @@ +{ + "type": "doc", + "value": { + "index": ".alerts-observability-apm", + "id": "NoxgpHkBqbdrfX07MqXV", + "source": { + "@timestamp": "2020-12-16T15:16:18.570Z", + "rule.id": "apm.error_rate", + "message": "hello world 1", + "kibana.rac.alert.owner": "apm", + "kibana.rac.alert.status": "open" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".alerts-security.alerts", + "id": "020202", + "source": { + "@timestamp": "2020-12-16T15:16:18.570Z", + "rule.id": "siem.signals", + "message": "hello world security", + "kibana.rac.alert.owner": "siem", + "kibana.rac.alert.status": "open" + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json b/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json new file mode 100644 index 00000000000000..4cb178d979982d --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json @@ -0,0 +1,47 @@ +{ + "type": "index", + "value": { + "index": ".alerts-observability-apm", + "mappings": { + "properties": { + "message": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "kibana.rac.alert.owner": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } +} + +{ + "type": "index", + "value": { + "index": ".alerts-security.alerts", + "mappings": { + "properties": { + "message": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "kibana.rac.alert.owner": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } +} diff --git a/x-pack/test/rule_registry/common/config.ts b/x-pack/test/rule_registry/common/config.ts new file mode 100644 index 00000000000000..8d1b3807a245b0 --- /dev/null +++ b/x-pack/test/rule_registry/common/config.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CA_CERT_PATH } from '@kbn/dev-utils'; +import { FtrConfigProviderContext } from '@kbn/test'; + +import { services } from './services'; +import { getAllExternalServiceSimulatorPaths } from '../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +interface CreateTestConfigOptions { + license: string; + disabledPlugins?: string[]; + ssl?: boolean; + testFiles?: string[]; +} + +// test.not-enabled is specifically not enabled +const enabledActionTypes = [ + '.email', + '.index', + '.jira', + '.pagerduty', + '.resilient', + '.server-log', + '.servicenow', + '.servicenow-sir', + '.slack', + '.webhook', + '.case', + 'test.authorization', + 'test.failing', + 'test.index-record', + 'test.noop', + 'test.rate-limit', +]; + +export function createTestConfig(name: string, options: CreateTestConfigOptions) { + const { license = 'trial', disabledPlugins = [], ssl = false, testFiles = [] } = options; + + return async ({ readConfigFile }: FtrConfigProviderContext) => { + const xPackApiIntegrationTestsConfig = await readConfigFile( + require.resolve('../../api_integration/config.ts') + ); + + const servers = { + ...xPackApiIntegrationTestsConfig.get('servers'), + elasticsearch: { + ...xPackApiIntegrationTestsConfig.get('servers.elasticsearch'), + protocol: ssl ? 'https' : 'http', + }, + }; + + return { + testFiles: testFiles ? testFiles : [require.resolve('../tests/common')], + servers, + services, + junit: { + reportName: 'X-Pack Rule Registry Alerts Client API Integration Tests', + }, + esTestCluster: { + ...xPackApiIntegrationTestsConfig.get('esTestCluster'), + license, + ssl, + serverArgs: [ + `xpack.license.self_generated.type=${license}`, + `xpack.security.enabled=${ + !disabledPlugins.includes('security') && ['trial', 'basic'].includes(license) + }`, + ], + }, + kbnTestServer: { + ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), + `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, + `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + '--xpack.eventLog.logEntries=true', + ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), + `--server.xsrf.whitelist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, + ...(ssl + ? [ + `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + ] + : []), + ], + }, + }; + }; +} diff --git a/x-pack/test/rule_registry/common/ftr_provider_context.d.ts b/x-pack/test/rule_registry/common/ftr_provider_context.d.ts new file mode 100644 index 00000000000000..aa56557c09df82 --- /dev/null +++ b/x-pack/test/rule_registry/common/ftr_provider_context.d.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GenericFtrProviderContext } from '@kbn/test'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/rule_registry/common/lib/authentication/index.ts b/x-pack/test/rule_registry/common/lib/authentication/index.ts new file mode 100644 index 00000000000000..f76159976a9025 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/authentication/index.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext as CommonFtrProviderContext } from '../../../common/ftr_provider_context'; +import { Role, User, UserInfo } from './types'; +import { allUsers } from './users'; +import { allRoles } from './roles'; +import { spaces } from './spaces'; + +export const getUserInfo = (user: User): UserInfo => ({ + username: user.username, + full_name: user.username.replace('_', ' '), + email: `${user.username}@elastic.co`, +}); + +export const createSpaces = async (getService: CommonFtrProviderContext['getService']) => { + const spacesService = getService('spaces'); + for (const space of spaces) { + await spacesService.create(space); + } +}; + +/** + * Creates the users and roles for use in the tests. Defaults to specific users and roles used by the security_and_spaces + * scenarios but can be passed specific ones as well. + */ +export const createUsersAndRoles = async ( + getService: CommonFtrProviderContext['getService'], + usersToCreate: User[] = allUsers, + rolesToCreate: Role[] = allRoles +) => { + const security = getService('security'); + + const createRole = async ({ name, privileges }: Role) => { + return security.role.create(name, privileges); + }; + + const createUser = async (user: User) => { + const userInfo = getUserInfo(user); + + return security.user.create(user.username, { + password: user.password, + roles: user.roles, + full_name: userInfo.full_name, + email: userInfo.email, + }); + }; + + for (const role of rolesToCreate) { + await createRole(role); + } + + for (const user of usersToCreate) { + await createUser(user); + } +}; + +export const deleteSpaces = async (getService: CommonFtrProviderContext['getService']) => { + const spacesService = getService('spaces'); + for (const space of spaces) { + try { + await spacesService.delete(space.id); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users + } + } +}; + +export const deleteUsersAndRoles = async ( + getService: CommonFtrProviderContext['getService'], + usersToDelete: User[] = allUsers, + rolesToDelete: Role[] = allRoles +) => { + const security = getService('security'); + + for (const user of usersToDelete) { + try { + await security.user.delete(user.username); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users + } + } + + for (const role of rolesToDelete) { + try { + await security.role.delete(role.name); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users + } + } +}; + +export const createSpacesAndUsers = async (getService: CommonFtrProviderContext['getService']) => { + await createSpaces(getService); + await createUsersAndRoles(getService); +}; + +export const deleteSpacesAndUsers = async (getService: CommonFtrProviderContext['getService']) => { + await deleteSpaces(getService); + await deleteUsersAndRoles(getService); +}; diff --git a/x-pack/test/rule_registry/common/lib/authentication/roles.ts b/x-pack/test/rule_registry/common/lib/authentication/roles.ts new file mode 100644 index 00000000000000..e38378dcfc8f21 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/authentication/roles.ts @@ -0,0 +1,435 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Role } from './types'; + +export const noKibanaPrivileges: Role = { + name: 'no_kibana_privileges', + privileges: { + elasticsearch: { + indices: [], + }, + }, +}; + +export const globalRead: Role = { + name: 'global_read', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + siem: ['read'], + apm: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const securitySolutionOnlyAll: Role = { + name: 'sec_only_all_spaces_space1', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + siem: ['all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const securitySolutionOnlyAllSpace2: Role = { + name: 'sec_only_all_spaces_space2', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + siem: ['all'], + }, + spaces: ['space2'], + }, + ], + }, +}; + +export const securitySolutionOnlyRead: Role = { + name: 'sec_only_read_spaces_space1', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + siem: ['read'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const securitySolutionOnlyReadSpace2: Role = { + name: 'sec_only_read_spaces_space2', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + siem: ['read'], + }, + spaces: ['space2'], + }, + ], + }, +}; + +export const observabilityOnlyAll: Role = { + name: 'obs_only_all_spaces_space1', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityOnlyAllSpace2: Role = { + name: 'obs_only_all_spaces_space2', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['all'], + }, + spaces: ['space2'], + }, + ], + }, +}; + +export const observabilityOnlyRead: Role = { + name: 'obs_only_read_spaces_space1', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['read'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityOnlyReadSpace2: Role = { + name: 'obs_only_read_spaces_space2', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['read'], + }, + spaces: ['space2'], + }, + ], + }, +}; + +/** + * These roles have access to all spaces. + */ +export const securitySolutionOnlyAllSpacesAll: Role = { + name: 'sec_only_all_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + siem: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const securitySolutionOnlyReadSpacesAll: Role = { + name: 'sec_only_read_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + siem: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const observabilityOnlyAllSpacesAll: Role = { + name: 'obs_only_all_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const observabilityOnlyReadSpacesAll: Role = { + name: 'obs_only_read_all_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const roles = [ + noKibanaPrivileges, + globalRead, + securitySolutionOnlyAll, + securitySolutionOnlyRead, + observabilityOnlyAll, + observabilityOnlyRead, + observabilityOnlyReadSpacesAll, +]; + +/** + * These roles are only to be used in the 'trial' tests + * since they rely on subfeature privileges which are a gold licencse feature + * maybe put these roles into a separate roles file like "trial_roles"? + */ +export const observabilityMinReadAlertsRead: Role = { + name: 'obs_only_alerts_read', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['minimal_read', 'alerts_read'], + ruleRegistry: ['all'], + actions: ['read'], + builtInAlerts: ['all'], + alerting: ['all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityMinReadAlertsReadSpacesAll: Role = { + name: 'obs_minimal_read_alerts_read_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['minimal_read', 'alerts_read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const observabilityMinimalRead: Role = { + name: 'obs_minimal_read', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['minimal_read'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityMinimalReadSpacesAll: Role = { + name: 'obs_minimal_read_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['minimal_read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +/** + * **************************************** + * These are used for testing update alerts privileges + * **************************************** + * **************************************** + * **************************************** + * **************************************** + * **************************************** + * **************************************** + * **************************************** + * **************************************** + */ + +export const observabilityMinReadAlertsAll: Role = { + name: 'obs_only_alerts_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['minimal_read', 'alerts_all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityMinReadAlertsAllSpacesAll: Role = { + name: 'obs_minimal_read_alerts_all_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['minimal_read', 'alerts_all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const observabilityMinimalAll: Role = { + name: 'obs_minimal_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['minimal_all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityMinimalAllSpacesAll: Role = { + name: 'obs_minimal_all_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['minimal_all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const allRoles = [ + noKibanaPrivileges, + globalRead, + securitySolutionOnlyAll, + securitySolutionOnlyRead, + observabilityOnlyAll, + observabilityOnlyRead, + securitySolutionOnlyAllSpacesAll, + securitySolutionOnlyReadSpacesAll, + observabilityOnlyAllSpacesAll, + observabilityOnlyReadSpacesAll, + observabilityMinReadAlertsRead, + observabilityMinReadAlertsReadSpacesAll, + observabilityMinimalRead, + observabilityMinimalReadSpacesAll, + observabilityMinReadAlertsAll, + observabilityMinReadAlertsAllSpacesAll, + observabilityMinimalAll, + observabilityMinimalAllSpacesAll, + securitySolutionOnlyAllSpace2, + securitySolutionOnlyReadSpace2, + observabilityOnlyAllSpace2, + observabilityOnlyReadSpace2, +]; diff --git a/x-pack/test/rule_registry/common/lib/authentication/spaces.ts b/x-pack/test/rule_registry/common/lib/authentication/spaces.ts new file mode 100644 index 00000000000000..556b1686601ffc --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/authentication/spaces.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Space } from './types'; + +const space1: Space = { + id: 'space1', + name: 'Space 1', + disabledFeatures: [], +}; + +const space2: Space = { + id: 'space2', + name: 'Space 2', + disabledFeatures: [], +}; + +export const spaces: Space[] = [space1, space2]; + +export const getSpaceUrlPrefix = (spaceId?: string) => { + return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; +}; diff --git a/x-pack/test/rule_registry/common/lib/authentication/types.ts b/x-pack/test/rule_registry/common/lib/authentication/types.ts new file mode 100644 index 00000000000000..3bf3629441f931 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/authentication/types.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface Space { + id: string; + namespace?: string; + name: string; + disabledFeatures: string[]; +} + +export interface User { + username: string; + password: string; + description?: string; + roles: string[]; +} + +export interface UserInfo { + username: string; + full_name: string; + email: string; +} + +interface FeaturesPrivileges { + [featureId: string]: string[]; +} + +interface ElasticsearchIndices { + names: string[]; + privileges: string[]; +} + +export interface ElasticSearchPrivilege { + cluster?: string[]; + indices?: ElasticsearchIndices[]; +} + +export interface KibanaPrivilege { + spaces: string[]; + base?: string[]; + feature?: FeaturesPrivileges; +} + +export interface Role { + name: string; + privileges: { + elasticsearch?: ElasticSearchPrivilege; + kibana?: KibanaPrivilege[]; + }; +} diff --git a/x-pack/test/rule_registry/common/lib/authentication/users.ts b/x-pack/test/rule_registry/common/lib/authentication/users.ts new file mode 100644 index 00000000000000..e142b3d1f56a34 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/authentication/users.ts @@ -0,0 +1,301 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + securitySolutionOnlyAll, + observabilityOnlyAll, + securitySolutionOnlyRead, + observabilityOnlyRead, + globalRead as globalReadRole, + noKibanaPrivileges as noKibanaPrivilegesRole, + securitySolutionOnlyAllSpacesAll, + securitySolutionOnlyReadSpacesAll, + observabilityOnlyAllSpacesAll, + observabilityOnlyReadSpacesAll, + // trial license roles + observabilityMinReadAlertsAll, + observabilityMinReadAlertsRead, + observabilityMinReadAlertsReadSpacesAll, + observabilityMinimalRead, + observabilityMinimalReadSpacesAll, + securitySolutionOnlyAllSpace2, + securitySolutionOnlyReadSpace2, + observabilityOnlyAllSpace2, + observabilityOnlyReadSpace2, + observabilityMinReadAlertsAllSpacesAll, +} from './roles'; +import { User } from './types'; + +export const superUser: User = { + username: 'superuser', + password: 'superuser', + roles: ['superuser'], +}; + +export const secOnly: User = { + username: 'sec_only_all_spaces_space1', + password: 'sec_only_all_spaces_space1', + roles: [securitySolutionOnlyAll.name], +}; + +export const secOnlySpace2: User = { + username: 'sec_only_all_spaces_space2', + password: 'sec_only_all_spaces_space2', + roles: [securitySolutionOnlyAllSpace2.name], +}; + +export const secOnlyRead: User = { + username: 'sec_only_read_spaces_space1', + password: 'sec_only_read_spaces_space1', + roles: [securitySolutionOnlyRead.name], +}; + +export const secOnlyReadSpace2: User = { + username: 'sec_only_read_spaces_space2', + password: 'sec_only_read_spaces_space2', + roles: [securitySolutionOnlyReadSpace2.name], +}; + +export const obsOnly: User = { + username: 'obs_only_all_spaces_space1', + password: 'obs_only_all_spaces_space1', + roles: [observabilityOnlyAll.name], +}; + +export const obsOnlySpace2: User = { + username: 'obs_only_all_spaces_space2', + password: 'obs_only_all_spaces_space2', + roles: [observabilityOnlyAllSpace2.name], +}; + +export const obsOnlyRead: User = { + username: 'obs_only_read_spaces_space1', + password: 'obs_only_read_spaces_space1', + roles: [observabilityOnlyRead.name], +}; + +export const obsOnlyReadSpace2: User = { + username: 'obs_only_read_spaces_space2', + password: 'obs_only_read_spaces_space2', + roles: [observabilityOnlyReadSpace2.name], +}; + +export const obsSec: User = { + username: 'sec_only_all_spaces_space1_and_obs_only_all_spaces_space1', + password: 'sec_only_all_spaces_space1_and_obs_only_all_spaces_space1', + roles: [securitySolutionOnlyAll.name, observabilityOnlyAll.name], +}; + +export const obsSecAllSpace2: User = { + username: 'sec_only_all_spaces_space2_and_obs_only_all_spaces_space2', + password: 'sec_only_all_spaces_space2_and_obs_only_all_spaces_space2', + roles: [securitySolutionOnlyAllSpace2.name, observabilityOnlyAllSpace2.name], +}; + +export const obsSecRead: User = { + username: 'sec_only_read_spaces_space1_and_obs_only_read_spaces_space1', + password: 'sec_only_read_spaces_space1_and_obs_only_read_spaces_space1', + roles: [securitySolutionOnlyRead.name, observabilityOnlyRead.name], +}; + +export const obsSecReadSpace2: User = { + username: 'sec_only_read_spaces_space2_and_obs_only_read_spaces_space2', + password: 'sec_only_read_spaces_space2_and_obs_only_read_spaces_space2', + roles: [securitySolutionOnlyReadSpace2.name, observabilityOnlyReadSpace2.name], +}; + +export const globalRead: User = { + username: 'global_read', + password: 'global_read', + roles: [globalReadRole.name], +}; + +export const noKibanaPrivileges: User = { + username: 'no_kibana_privileges', + password: 'no_kibana_privileges', + roles: [noKibanaPrivilegesRole.name], +}; + +export const obsOnlyReadSpacesAll: User = { + username: 'obs_only_read_all_spaces_all', + password: 'obs_only_read_all_spaces_all', + roles: [observabilityOnlyReadSpacesAll.name], +}; + +export const users = [ + superUser, + secOnly, + secOnlyRead, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, +]; + +/** + * These users will have access to all spaces. + */ + +export const secOnlySpacesAll: User = { + username: 'sec_only_all_spaces_all', + password: 'sec_only_all_spaces_all', + roles: [securitySolutionOnlyAllSpacesAll.name], +}; + +export const secOnlyReadSpacesAll: User = { + username: 'sec_only_read_spaces_all', + password: 'sec_only_read_spaces_all', + roles: [securitySolutionOnlyReadSpacesAll.name], +}; + +export const obsOnlySpacesAll: User = { + username: 'obs_only_all_spaces_all', + password: 'obs_only_all_spaces_all', + roles: [observabilityOnlyAllSpacesAll.name], +}; + +export const obsSecSpacesAll: User = { + username: 'sec_only_all_spaces_all_and_obs_only_all_spaces_all', + password: 'sec_only_all_spaces_all_and_obs_only_all_spaces_all', + roles: [securitySolutionOnlyAllSpacesAll.name, observabilityOnlyAllSpacesAll.name], +}; + +export const obsSecReadSpacesAll: User = { + username: 'sec_only_read_all_spaces_all_and_obs_only_read_all_spaces_all', + password: 'sec_only_read_all_spaces_all_and_obs_only_read_all_spaces_all', + roles: [securitySolutionOnlyReadSpacesAll.name, observabilityOnlyReadSpacesAll.name], +}; + +/** + * These users are for the security_only tests because most of them have access to the default space instead of 'space1' + */ +export const usersDefaultSpace = [ + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsOnlySpacesAll, + obsOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + globalRead, + noKibanaPrivileges, +]; + +/** + * Trial users with trial roles + */ + +// apm: ['minimal_read', 'alerts_read'] +// spaces: ['space1'] +export const obsMinReadAlertsRead: User = { + username: 'obs_minimal_read_alerts_read_single_space', + password: 'obs_minimal_read_alerts_read_single_space', + roles: [observabilityMinReadAlertsRead.name], +}; + +// apm: ['minimal_read', 'alerts_read'] +// spaces: ['*'] +export const obsMinReadAlertsReadSpacesAll: User = { + username: 'obs_minimal_read_alerts_read_all_spaces', + password: 'obs_minimal_read_alerts_read_all_spaces', + roles: [observabilityMinReadAlertsReadSpacesAll.name], +}; + +// apm: ['minimal_read'] +// spaces: ['space1'] +export const obsMinRead: User = { + username: 'obs_minimal_read_single_space', + password: 'obs_minimal_read_single_space', + roles: [observabilityMinimalRead.name], +}; + +// apm: ['minimal_read'] +// spaces: ['*'] +export const obsMinReadSpacesAll: User = { + username: 'obs_minimal_read_all_space', + password: 'obs_minimal_read_all_space', + roles: [observabilityMinimalReadSpacesAll.name], +}; + +// FOR UPDATES +// apm: ['minimal_read', 'alerts_all'] +// spaces: ['space1'] +export const obsMinReadAlertsAll: User = { + username: 'obs_minimal_read_alerts_all_single_space', + password: 'obs_minimal_read_alerts_all_single_space', + roles: [observabilityMinReadAlertsAll.name], +}; + +// apm: ['minimal_read', 'alerts_all'] +// spaces: ['*'] +export const obsMinReadAlertsAllSpacesAll: User = { + username: 'obs_minimal_read_alerts_all_all_spaces', + password: 'obs_minimal_read_alerts_all_all_spaces', + roles: [observabilityMinReadAlertsAllSpacesAll.name], +}; + +// apm: ['minimal_all'] +// spaces: ['space1'] +export const obsMinAll: User = { + username: 'obs_minimal_all_single_space', + password: 'obs_minimal_all_single_space', + roles: [observabilityMinimalRead.name], +}; + +// apm: ['minimal_all'] +// spaces: ['*'] +export const obsMinAllSpacesAll: User = { + username: 'obs_minimal_all_all_space', + password: 'obs_minimal_read_all_space', + roles: [observabilityMinimalReadSpacesAll.name], +}; + +export const trialUsers = [ + obsMinReadAlertsRead, + obsMinReadAlertsReadSpacesAll, + obsMinRead, + obsMinReadSpacesAll, + obsMinReadAlertsAll, + obsMinReadAlertsAllSpacesAll, + obsMinAll, + obsMinAllSpacesAll, +]; + +export const allUsers = [ + superUser, + secOnly, + secOnlyRead, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsOnlySpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + obsMinReadAlertsRead, + obsMinReadAlertsReadSpacesAll, + obsMinRead, + obsMinReadSpacesAll, + obsMinReadAlertsAll, + obsMinReadAlertsAllSpacesAll, + obsMinAll, + obsMinAllSpacesAll, + secOnlySpace2, + secOnlyReadSpace2, + obsOnlySpace2, + obsOnlyReadSpace2, + obsSecAllSpace2, + obsSecReadSpace2, +]; diff --git a/x-pack/test/rule_registry/common/services.ts b/x-pack/test/rule_registry/common/services.ts new file mode 100644 index 00000000000000..7e415338c405f9 --- /dev/null +++ b/x-pack/test/rule_registry/common/services.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { services } from '../../api_integration/services'; diff --git a/x-pack/test/rule_registry/security_and_spaces/config_basic.ts b/x-pack/test/rule_registry/security_and_spaces/config_basic.ts new file mode 100644 index 00000000000000..98b7b1abe98e78 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/config_basic.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + license: 'basic', + ssl: true, + testFiles: [require.resolve('./tests/basic')], +}); diff --git a/x-pack/test/rule_registry/security_and_spaces/config_trial.ts b/x-pack/test/rule_registry/security_and_spaces/config_trial.ts new file mode 100644 index 00000000000000..b5328fd83c2cbc --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/config_trial.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + license: 'trial', + ssl: true, + testFiles: [require.resolve('./tests/trial')], +}); diff --git a/x-pack/test/rule_registry/security_and_spaces/roles_users_utils/index.ts b/x-pack/test/rule_registry/security_and_spaces/roles_users_utils/index.ts new file mode 100644 index 00000000000000..b320446cbe05f7 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/roles_users_utils/index.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { assertUnreachable } from '../../../../plugins/security_solution/common/utility_types'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + t1AnalystUser, + t2AnalystUser, + hunterUser, + ruleAuthorUser, + socManagerUser, + platformEngineerUser, + detectionsAdminUser, + readerUser, + t1AnalystRole, + t2AnalystRole, + hunterRole, + ruleAuthorRole, + socManagerRole, + platformEngineerRole, + detectionsAdminRole, + readerRole, +} from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users'; + +import { ROLES } from '../../../../plugins/security_solution/common/test'; + +export const createUserAndRole = async ( + getService: FtrProviderContext['getService'], + role: ROLES +): Promise => { + switch (role) { + case ROLES.detections_admin: + return postRoleAndUser( + ROLES.detections_admin, + detectionsAdminRole, + detectionsAdminUser, + getService + ); + case ROLES.t1_analyst: + return postRoleAndUser(ROLES.t1_analyst, t1AnalystRole, t1AnalystUser, getService); + case ROLES.t2_analyst: + return postRoleAndUser(ROLES.t2_analyst, t2AnalystRole, t2AnalystUser, getService); + case ROLES.hunter: + return postRoleAndUser(ROLES.hunter, hunterRole, hunterUser, getService); + case ROLES.rule_author: + return postRoleAndUser(ROLES.rule_author, ruleAuthorRole, ruleAuthorUser, getService); + case ROLES.soc_manager: + return postRoleAndUser(ROLES.soc_manager, socManagerRole, socManagerUser, getService); + case ROLES.platform_engineer: + return postRoleAndUser( + ROLES.platform_engineer, + platformEngineerRole, + platformEngineerUser, + getService + ); + case ROLES.reader: + return postRoleAndUser(ROLES.reader, readerRole, readerUser, getService); + default: + return assertUnreachable(role); + } +}; + +/** + * Given a roleName and security service this will delete the roleName + * and user + * @param roleName The user and role to delete with the same name + * @param securityService The security service + */ +export const deleteUserAndRole = async ( + getService: FtrProviderContext['getService'], + roleName: ROLES +): Promise => { + const securityService = getService('security'); + await securityService.user.delete(roleName); + await securityService.role.delete(roleName); +}; + +interface UserInterface { + password: string; + roles: string[]; + full_name: string; + email: string; +} + +interface RoleInterface { + elasticsearch: { + cluster: string[]; + indices: Array<{ + names: string[]; + privileges: string[]; + }>; + }; + kibana: Array<{ + feature: { + ml: string[]; + siem: string[]; + actions: string[]; + builtInAlerts: string[]; + }; + spaces: string[]; + }>; +} + +export const postRoleAndUser = async ( + roleName: string, + role: RoleInterface, + user: UserInterface, + getService: FtrProviderContext['getService'] +): Promise => { + const securityService = getService('security'); + await securityService.role.create(roleName, { + kibana: role.kibana, + elasticsearch: role.elasticsearch, + }); + await securityService.user.create(roleName, { + password: 'changeme', + full_name: user.full_name, + roles: user.roles, + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts new file mode 100644 index 00000000000000..cf3cc88f2cfc01 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts @@ -0,0 +1,210 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { + superUser, + globalRead, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + secOnlySpace2, + secOnlyReadSpace2, + obsSecAllSpace2, + obsSecReadSpace2, + obsOnlySpace2, + obsOnlyReadSpace2, + obsOnlySpacesAll, + obsSecSpacesAll, + secOnlySpacesAll, + noKibanaPrivileges, +} from '../../../common/lib/authentication/users'; +import type { User } from '../../../common/lib/authentication/types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +interface TestCase { + /** The space where the alert exists */ + space: string; + /** The ID of the alert */ + alertId: string; + /** The index of the alert */ + index: string; + /** Authorized users */ + authorizedUsers: User[]; + /** Unauthorized users */ + unauthorizedUsers: User[]; +} + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/internal/rac/alerts'; + const ALERTS_INDEX_URL = `${TEST_URL}/index`; + const SPACE1 = 'space1'; + const SPACE2 = 'space2'; + const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV'; + const APM_ALERT_INDEX = '.alerts-observability-apm'; + const SECURITY_SOLUTION_ALERT_ID = '020202'; + const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts'; + + const getAPMIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(200); + const observabilityIndex = indexNames?.index_name?.find( + (indexName) => indexName === APM_ALERT_INDEX + ); + expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + const getSecuritySolutionIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(200); + const securitySolution = indexNames?.index_name?.find( + (indexName) => indexName === SECURITY_SOLUTION_ALERT_INDEX + ); + expect(securitySolution).to.eql(SECURITY_SOLUTION_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + describe('Alerts - GET - RBAC - spaces', () => { + before(async () => { + await getSecuritySolutionIndexName(superUser); + await getAPMIndexName(superUser); + + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + function addTests({ space, authorizedUsers, unauthorizedUsers, alertId, index }: TestCase) { + authorizedUsers.forEach(({ username, password }) => { + it(`${username} should be able to access alert ${alertId} in ${space}/${index}`, async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space)}${TEST_URL}?id=${alertId}&index=${index}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + }); + + it(`${username} should fail to access a non-existent alert in ${space}/${index}`, async () => { + const fakeAlertId = 'some-alert-id-that-doesnt-exist'; + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space)}${TEST_URL}?id=${fakeAlertId}&index=${index}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(404); + }); + + it(`${username} should return a 404 when trying to accesses not-existent alerts as data index`, async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space)}${TEST_URL}?id=${APM_ALERT_ID}&index=myfakeindex`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(404); + }); + }); + + unauthorizedUsers.forEach(({ username, password }) => { + it(`${username} should NOT be able to access alert ${alertId} in ${space}/${index}`, async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space)}${TEST_URL}?id=${alertId}&index=${index}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(403); + }); + }); + } + + describe('Security Solution', () => { + const authorizedInAllSpaces = [superUser, globalRead, secOnlySpacesAll, obsSecSpacesAll]; + const authorizedOnlyInSpace1 = [secOnly, secOnlyRead, obsSec, obsSecRead]; + const authorizedOnlyInSpace2 = [ + secOnlySpace2, + secOnlyReadSpace2, + obsSecAllSpace2, + obsSecReadSpace2, + ]; + const unauthorized = [ + // these users are not authorized to access alerts for the Security Solution in any space + obsOnly, + obsOnlyRead, + obsOnlySpace2, + obsOnlyReadSpace2, + obsOnlySpacesAll, + noKibanaPrivileges, + ]; + + addTests({ + space: SPACE1, + alertId: SECURITY_SOLUTION_ALERT_ID, + index: SECURITY_SOLUTION_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace1], + unauthorizedUsers: [...authorizedOnlyInSpace2, ...unauthorized], + }); + addTests({ + space: SPACE2, + alertId: SECURITY_SOLUTION_ALERT_ID, + index: SECURITY_SOLUTION_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace2], + unauthorizedUsers: [...authorizedOnlyInSpace1, ...unauthorized], + }); + }); + + describe('APM', () => { + const authorizedInAllSpaces = [superUser, globalRead, obsOnlySpacesAll, obsSecSpacesAll]; + const authorizedOnlyInSpace1 = [obsOnly, obsOnlyRead, obsSec, obsSecRead]; + const authorizedOnlyInSpace2 = [ + obsOnlySpace2, + obsOnlyReadSpace2, + obsSecAllSpace2, + obsSecReadSpace2, + ]; + const unauthorized = [ + // these users are not authorized to access alerts for APM in any space + secOnly, + secOnlyRead, + secOnlySpace2, + secOnlyReadSpace2, + secOnlySpacesAll, + noKibanaPrivileges, + ]; + + addTests({ + space: SPACE1, + alertId: APM_ALERT_ID, + index: APM_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace1], + unauthorizedUsers: [...authorizedOnlyInSpace2, ...unauthorized], + }); + addTests({ + space: SPACE2, + alertId: APM_ALERT_ID, + index: APM_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace2], + unauthorizedUsers: [...authorizedOnlyInSpace1, ...unauthorized], + }); + }); + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts new file mode 100644 index 00000000000000..baea62c1572180 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile, getService }: FtrProviderContext): void => { + describe('rules security and spaces enabled: basic', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + before(async () => { + await createSpacesAndUsers(getService); + }); + + after(async () => { + await deleteSpacesAndUsers(getService); + }); + + // Basic + loadTestFile(require.resolve('./get_alert_by_id')); + loadTestFile(require.resolve('./update_alert')); + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts new file mode 100644 index 00000000000000..4fb087e8137680 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts @@ -0,0 +1,251 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; + +import { + superUser, + globalRead, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + secOnlySpace2, + secOnlyReadSpace2, + obsSecAllSpace2, + obsSecReadSpace2, + obsOnlySpace2, + obsOnlyReadSpace2, + obsOnlySpacesAll, + obsSecSpacesAll, + secOnlySpacesAll, + noKibanaPrivileges, +} from '../../../common/lib/authentication/users'; +import type { User } from '../../../common/lib/authentication/types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +interface TestCase { + /** The space where the alert exists */ + space: string; + /** The ID of the alert */ + alertId: string; + /** The index of the alert */ + index: string; + /** Authorized users */ + authorizedUsers: User[]; + /** Unauthorized users */ + unauthorizedUsers: User[]; +} + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/internal/rac/alerts'; + const ALERTS_INDEX_URL = `${TEST_URL}/index`; + const SPACE1 = 'space1'; + const SPACE2 = 'space2'; + const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV'; + const APM_ALERT_INDEX = '.alerts-observability-apm'; + const SECURITY_SOLUTION_ALERT_ID = '020202'; + const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts'; + const ALERT_VERSION = Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'); // required for optimistic concurrency control + + const getAPMIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(200); + const observabilityIndex = indexNames?.index_name?.find( + (indexName) => indexName === APM_ALERT_INDEX + ); + expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + const getSecuritySolutionIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(200); + const securitySolution = indexNames?.index_name?.find( + (indexName) => indexName === SECURITY_SOLUTION_ALERT_INDEX + ); + expect(securitySolution).to.eql(SECURITY_SOLUTION_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + describe('Alert - Update - RBAC - spaces', () => { + before(async () => { + await getSecuritySolutionIndexName(superUser); + await getAPMIndexName(superUser); + }); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + function addTests({ space, authorizedUsers, unauthorizedUsers, alertId, index }: TestCase) { + authorizedUsers.forEach(({ username, password }) => { + it(`${username} should be able to update alert ${alertId} in ${space}/${index}`, async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); // since this is a success case, reload the test data immediately beforehand + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(space)}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send({ + ids: [alertId], + status: 'closed', + index, + _version: ALERT_VERSION, + }) + .expect(200); + }); + + it(`${username} should fail to update alert ${alertId} in ${space}/${index} with an incorrect version`, async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(space)}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send({ + ids: [alertId], + status: 'closed', + index, + _version: Buffer.from(JSON.stringify([999, 999]), 'utf8').toString('base64'), + }) + .expect(409); + }); + + it(`${username} should fail to update a non-existent alert in ${space}/${index}`, async () => { + const fakeAlertId = 'some-alert-id-that-doesnt-exist'; + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(space)}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send({ + ids: [fakeAlertId], + status: 'closed', + index, + _version: ALERT_VERSION, + }) + .expect(404); + }); + + it(`${username} should return a 404 when superuser accesses not-existent alerts as data index`, async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space)}${TEST_URL}?id=${APM_ALERT_ID}&index=myfakeindex`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send({ + ids: [APM_ALERT_ID], + status: 'closed', + index: 'this index does not exist', + _version: ALERT_VERSION, + }) + .expect(404); + }); + }); + + unauthorizedUsers.forEach(({ username, password }) => { + it(`${username} should NOT be able to update alert ${alertId} in ${space}/${index}`, async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(space)}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send({ + ids: [alertId], + status: 'closed', + index, + _version: ALERT_VERSION, + }) + .expect(403); + }); + }); + } + + describe('Security Solution', () => { + const authorizedInAllSpaces = [superUser, secOnlySpacesAll, obsSecSpacesAll]; + const authorizedOnlyInSpace1 = [secOnly, obsSec]; + const authorizedOnlyInSpace2 = [secOnlySpace2, obsSecAllSpace2]; + const unauthorized = [ + // these users are not authorized to update alerts for the Security Solution in any space + globalRead, + secOnlyRead, + obsSecRead, + secOnlyReadSpace2, + obsSecReadSpace2, + obsOnly, + obsOnlyRead, + obsOnlySpace2, + obsOnlyReadSpace2, + obsOnlySpacesAll, + noKibanaPrivileges, + ]; + + addTests({ + space: SPACE1, + alertId: SECURITY_SOLUTION_ALERT_ID, + index: SECURITY_SOLUTION_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace1], + unauthorizedUsers: [...authorizedOnlyInSpace2, ...unauthorized], + }); + addTests({ + space: SPACE2, + alertId: SECURITY_SOLUTION_ALERT_ID, + index: SECURITY_SOLUTION_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace2], + unauthorizedUsers: [...authorizedOnlyInSpace1, ...unauthorized], + }); + }); + + describe('APM', () => { + const authorizedInAllSpaces = [superUser, obsOnlySpacesAll, obsSecSpacesAll]; + const authorizedOnlyInSpace1 = [obsOnly, obsSec]; + const authorizedOnlyInSpace2 = [obsOnlySpace2, obsSecAllSpace2]; + const unauthorized = [ + // these users are not authorized to update alerts for APM in any space + globalRead, + obsOnlyRead, + obsSecRead, + obsOnlyReadSpace2, + obsSecReadSpace2, + secOnly, + secOnlyRead, + secOnlySpace2, + secOnlyReadSpace2, + secOnlySpacesAll, + noKibanaPrivileges, + ]; + + addTests({ + space: SPACE1, + alertId: APM_ALERT_ID, + index: APM_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace1], + unauthorizedUsers: [...authorizedOnlyInSpace2, ...unauthorized], + }); + addTests({ + space: SPACE2, + alertId: APM_ALERT_ID, + index: APM_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace2], + unauthorizedUsers: [...authorizedOnlyInSpace1, ...unauthorized], + }); + }); + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/trial/get_alerts.ts b/x-pack/test/rule_registry/security_and_spaces/tests/trial/get_alerts.ts new file mode 100644 index 00000000000000..a38f6cf3263b19 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/trial/get_alerts.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { + superUser, + obsMinReadSpacesAll, + obsMinRead, + obsMinReadAlertsRead, + obsMinReadAlertsReadSpacesAll, +} from '../../../common/lib/authentication/users'; +import type { User } from '../../../common/lib/authentication/types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/internal/rac/alerts'; + const ALERTS_INDEX_URL = `${TEST_URL}/index`; + const SPACE1 = 'space1'; + const SPACE2 = 'space2'; + + const getAPMIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(200); + const observabilityIndex = indexNames?.index_name?.find( + (indexName) => indexName === '.alerts-observability-apm' + ); + expect(observabilityIndex).to.eql('.alerts-observability-apm'); + return observabilityIndex; + }; + + describe('rbac with subfeatures', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + describe('Users:', () => { + // user with minimal_read and alerts_read privileges should be able to access apm alert + it(`${obsMinReadAlertsRead.username} should be able to access the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&index=${apmIndex}`) + .auth(obsMinReadAlertsRead.username, obsMinReadAlertsRead.password) + .set('kbn-xsrf', 'true') + .expect(200); + }); + it(`${obsMinReadAlertsReadSpacesAll.username} should be able to access the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&index=${apmIndex}`) + .auth(obsMinReadAlertsReadSpacesAll.username, obsMinReadAlertsReadSpacesAll.password) + .set('kbn-xsrf', 'true') + .expect(200); + }); + + it(`${obsMinRead.username} should NOT be able to access the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&index=${apmIndex}`) + .auth(obsMinRead.username, obsMinRead.password) + .set('kbn-xsrf', 'true') + .expect(403); + }); + + it(`${obsMinReadSpacesAll.username} should NOT be able to access the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&index=${apmIndex}`) + .auth(obsMinReadSpacesAll.username, obsMinReadSpacesAll.password) + .set('kbn-xsrf', 'true') + .expect(403); + }); + }); + + describe('Space:', () => { + it(`${obsMinReadAlertsRead.username} should NOT be able to access the APM alert in ${SPACE2}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE2)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&index=${apmIndex}`) + .auth(obsMinReadAlertsRead.username, obsMinReadAlertsRead.password) + .set('kbn-xsrf', 'true') + .expect(403); + }); + + it(`${obsMinReadAlertsReadSpacesAll.username} should be able to access the APM alert in ${SPACE2}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE2)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&index=${apmIndex}`) + .auth(obsMinReadAlertsReadSpacesAll.username, obsMinReadAlertsReadSpacesAll.password) + .set('kbn-xsrf', 'true') + .expect(200); + }); + + describe('extra params', () => { + it('should NOT allow to pass a filter query parameter', async () => { + await supertest + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?sortOrder=asc&namespaces[0]=*`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + it('should NOT allow to pass a non supported query parameter', async () => { + await supertest + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?notExists=something`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + }); + }); + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/trial/index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/trial/index.ts new file mode 100644 index 00000000000000..5e89f99200f2db --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/trial/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createSpaces, + createUsersAndRoles, + deleteSpaces, + deleteUsersAndRoles, +} from '../../../common/lib/authentication'; + +import { + observabilityMinReadAlertsRead, + observabilityMinReadAlertsReadSpacesAll, + observabilityMinimalRead, + observabilityMinimalReadSpacesAll, + observabilityMinReadAlertsAll, + observabilityMinReadAlertsAllSpacesAll, + observabilityMinimalAll, + observabilityMinimalAllSpacesAll, +} from '../../../common/lib/authentication/roles'; +import { + obsMinReadAlertsRead, + obsMinReadAlertsReadSpacesAll, + obsMinRead, + obsMinReadSpacesAll, + superUser, + obsMinReadAlertsAll, + obsMinReadAlertsAllSpacesAll, + obsMinAll, + obsMinAllSpacesAll, +} from '../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile, getService }: FtrProviderContext): void => { + describe('rules security and spaces enabled: trial', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + before(async () => { + await createSpaces(getService); + await createUsersAndRoles( + getService, + [ + obsMinReadAlertsRead, + obsMinReadAlertsReadSpacesAll, + obsMinRead, + obsMinReadSpacesAll, + superUser, + obsMinReadAlertsAll, + obsMinReadAlertsAllSpacesAll, + obsMinAll, + obsMinAllSpacesAll, + ], + [ + observabilityMinReadAlertsRead, + observabilityMinReadAlertsReadSpacesAll, + observabilityMinimalRead, + observabilityMinimalReadSpacesAll, + observabilityMinReadAlertsAll, + observabilityMinReadAlertsAllSpacesAll, + observabilityMinimalAll, + observabilityMinimalAllSpacesAll, + ] + ); + }); + + after(async () => { + await deleteSpaces(getService); + await deleteUsersAndRoles( + getService, + [ + obsMinReadAlertsRead, + obsMinReadAlertsReadSpacesAll, + obsMinRead, + obsMinReadSpacesAll, + superUser, + obsMinReadAlertsAll, + obsMinReadAlertsAllSpacesAll, + obsMinAll, + obsMinAllSpacesAll, + ], + [ + observabilityMinReadAlertsRead, + observabilityMinReadAlertsReadSpacesAll, + observabilityMinimalRead, + observabilityMinimalReadSpacesAll, + observabilityMinReadAlertsAll, + observabilityMinReadAlertsAllSpacesAll, + observabilityMinimalAll, + observabilityMinimalAllSpacesAll, + ] + ); + }); + + // Trial + loadTestFile(require.resolve('./get_alerts')); + loadTestFile(require.resolve('./update_alert')); + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/trial/update_alert.ts b/x-pack/test/rule_registry/security_and_spaces/tests/trial/update_alert.ts new file mode 100644 index 00000000000000..c126c434bd4cf2 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/trial/update_alert.ts @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; + +import { + superUser, + obsMinReadAlertsAll, + obsMinReadAlertsAllSpacesAll, + obsMinAll, + obsMinAllSpacesAll, +} from '../../../common/lib/authentication/users'; +import type { User } from '../../../common/lib/authentication/types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/internal/rac/alerts'; + const ALERTS_INDEX_URL = `${TEST_URL}/index`; + const SPACE1 = 'space1'; + + const getAPMIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(200); + const observabilityIndex = indexNames?.index_name?.find( + (indexName) => indexName === '.alerts-observability-apm' + ); + expect(observabilityIndex).to.eql('.alerts-observability-apm'); + return observabilityIndex; + }; + + describe('rbac', () => { + describe('Users update:', () => { + beforeEach(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + afterEach(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + it(`${superUser.username} should be able to update the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(200); + }); + + it(`${superUser.username} should receive a 409 if trying to update an old alert document version`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(200); + + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([999, 999]), 'utf8').toString('base64'), + }) + .expect(409); + }); + + it(`${obsMinReadAlertsAllSpacesAll.username} should be able to update the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + const res = await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(obsMinReadAlertsAllSpacesAll.username, obsMinReadAlertsAllSpacesAll.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(200); + expect(res.body).to.eql({ + success: true, + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + result: 'updated', + _shards: { total: 2, successful: 1, failed: 0 }, + _version: 'WzEsMV0=', + _seq_no: 1, + _primary_term: 1, + }); + }); + it(`${obsMinReadAlertsAllSpacesAll.username} should receive a 409 if trying to update an old alert document version`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(obsMinReadAlertsAllSpacesAll.username, obsMinReadAlertsAllSpacesAll.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(200); + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(obsMinReadAlertsAllSpacesAll.username, obsMinReadAlertsAllSpacesAll.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([999, 999]), 'utf8').toString('base64'), + }) + .expect(409); + }); + + it(`${obsMinReadAlertsAll.username} should be able to update the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(obsMinReadAlertsAll.username, obsMinReadAlertsAll.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(200); + }); + it(`${obsMinAll.username} should NOT be able to update the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(obsMinAll.username, obsMinAll.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(403); + }); + + it(`${obsMinAllSpacesAll.username} should NOT be able to update the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(obsMinAllSpacesAll.username, obsMinAllSpacesAll.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(403); + }); + }); + }); +}; diff --git a/x-pack/test/rule_registry/spaces_only/config_trial.ts b/x-pack/test/rule_registry/spaces_only/config_trial.ts new file mode 100644 index 00000000000000..e788a16d0272fa --- /dev/null +++ b/x-pack/test/rule_registry/spaces_only/config_trial.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('spaces_only', { + license: 'trial', + disabledPlugins: ['security'], + ssl: false, + testFiles: [require.resolve('./tests/trial')], +}); diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/get_alert_by_id.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/get_alert_by_id.ts new file mode 100644 index 00000000000000..df188718bff192 --- /dev/null +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/get_alert_by_id.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { superUser } from '../../../common/lib/authentication/users'; +import type { User } from '../../../common/lib/authentication/types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/internal/rac/alerts'; + const ALERTS_INDEX_URL = `${TEST_URL}/index`; + const SPACE1 = 'space1'; + const SPACE2 = 'space2'; + const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV'; + const APM_ALERT_INDEX = '.alerts-observability-apm'; + const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts'; + + const getAPMIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .set('kbn-xsrf', 'true') + .expect(200); + const observabilityIndex = indexNames?.index_name?.find( + (indexName) => indexName === APM_ALERT_INDEX + ); + expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + const getSecuritySolutionIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .set('kbn-xsrf', 'true') + .expect(200); + const securitySolution = indexNames?.index_name?.find( + (indexName) => indexName === SECURITY_SOLUTION_ALERT_INDEX + ); + expect(securitySolution).to.eql(SECURITY_SOLUTION_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + describe('Alerts - GET - RBAC - spaces', () => { + before(async () => { + await getSecuritySolutionIndexName(superUser); + await getAPMIndexName(superUser); + + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + it('should return a 404 when superuser accesses not-existent alert', async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix()}${TEST_URL}?id=myfakeid&index=${APM_ALERT_INDEX}`) + .set('kbn-xsrf', 'true') + .expect(404); + }); + + it('should return a 404 when superuser accesses not-existent alerts as data index', async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix()}${TEST_URL}?id=${APM_ALERT_ID}&index=myfakeindex`) + .set('kbn-xsrf', 'true') + .expect(404); + }); + + it(`${superUser.username} should be able to access alert ${APM_ALERT_ID} in ${SPACE2}/${APM_ALERT_INDEX}`, async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE2)}${TEST_URL}?id=${APM_ALERT_ID}&index=${APM_ALERT_INDEX}`) + .set('kbn-xsrf', 'true') + .expect(200); + }); + }); +}; diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts new file mode 100644 index 00000000000000..6deba4c68d0e26 --- /dev/null +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { createSpaces, deleteSpaces } from '../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile, getService }: FtrProviderContext): void => { + describe('rule registry spaces only: trial', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + before(async () => { + await createSpaces(getService); + }); + + after(async () => { + await deleteSpaces(getService); + }); + + // Basic + loadTestFile(require.resolve('./get_alert_by_id')); + loadTestFile(require.resolve('./update_alert')); + }); +}; diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/update_alert.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/update_alert.ts new file mode 100644 index 00000000000000..f5179b253b7017 --- /dev/null +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/update_alert.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; + +import { superUser } from '../../../common/lib/authentication/users'; +import type { User } from '../../../common/lib/authentication/types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/internal/rac/alerts'; + const ALERTS_INDEX_URL = `${TEST_URL}/index`; + const SPACE1 = 'space1'; + const SPACE2 = 'space2'; + const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV'; + const APM_ALERT_INDEX = '.alerts-observability-apm'; + const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts'; + const ALERT_VERSION = Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'); // required for optimistic concurrency control + + const getAPMIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .set('kbn-xsrf', 'true') + .expect(200); + const observabilityIndex = indexNames?.index_name?.find( + (indexName) => indexName === APM_ALERT_INDEX + ); + expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + const getSecuritySolutionIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .set('kbn-xsrf', 'true') + .expect(200); + const securitySolution = indexNames?.index_name?.find( + (indexName) => indexName === SECURITY_SOLUTION_ALERT_INDEX + ); + expect(securitySolution).to.eql(SECURITY_SOLUTION_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + describe('Alert - Update - RBAC - spaces', () => { + before(async () => { + await getSecuritySolutionIndexName(superUser); + await getAPMIndexName(superUser); + }); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + it('should return a 404 when superuser accesses not-existent alert', async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix()}${TEST_URL}`) + .set('kbn-xsrf', 'true') + .send({ + ids: ['this id does not exist'], + status: 'closed', + index: APM_ALERT_INDEX, + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(404); + }); + + it('should return a 404 when superuser accesses not-existent alerts as data index', async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix()}${TEST_URL}`) + .set('kbn-xsrf', 'true') + .send({ + ids: [APM_ALERT_ID], + status: 'closed', + index: 'this index does not exist', + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(404); + }); + + it(`${superUser.username} should be able to update alert ${APM_ALERT_ID} in ${SPACE2}/${APM_ALERT_INDEX}`, async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); // since this is a success case, reload the test data immediately beforehand + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE2)}${TEST_URL}`) + .set('kbn-xsrf', 'true') + .send({ + ids: [APM_ALERT_ID], + status: 'closed', + index: APM_ALERT_INDEX, + _version: ALERT_VERSION, + }) + .expect(200); + }); + }); +}; From 0af424e04e070eade991c8f40343e7b004c982d3 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 8 Jul 2021 12:26:41 -0700 Subject: [PATCH 20/77] add missing await (#104941) Co-authored-by: spalger --- .../test/functional/apps/monitoring/elasticsearch/nodes_mb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/nodes_mb.js b/x-pack/test/functional/apps/monitoring/elasticsearch/nodes_mb.js index 7e9a0fec708241..a031b828e2632c 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/nodes_mb.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/nodes_mb.js @@ -263,7 +263,7 @@ export default function ({ getService, getPageObjects }) { } ); - overview.closeAlertsModal(); + await overview.closeAlertsModal(); // go to nodes listing await overview.clickEsNodes(); From f2a94addc859da5bb01f9352b0eed09abe415193 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Thu, 8 Jul 2021 15:29:16 -0400 Subject: [PATCH 21/77] [APM] Fleet migration - Set APM Server URL from Cloud plugin (#104907) (#104918) --- .../fleet/create_cloud_apm_package_policy.ts | 14 +++++++--- .../get_apm_package_policy_definition.ts | 27 +++++++++++++------ x-pack/plugins/apm/server/routes/fleet.ts | 2 ++ 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/fleet/create_cloud_apm_package_policy.ts b/x-pack/plugins/apm/server/lib/fleet/create_cloud_apm_package_policy.ts index 9e3095a8d1bcab..c336e5dc95ba65 100644 --- a/x-pack/plugins/apm/server/lib/fleet/create_cloud_apm_package_policy.ts +++ b/x-pack/plugins/apm/server/lib/fleet/create_cloud_apm_package_policy.ts @@ -14,15 +14,20 @@ import { APM_SERVER_SCHEMA_SAVED_OBJECT_TYPE, APM_SERVER_SCHEMA_SAVED_OBJECT_ID, } from '../../../common/apm_saved_object_constants'; -import { APMPluginStartDependencies } from '../../types'; +import { + APMPluginSetupDependencies, + APMPluginStartDependencies, +} from '../../types'; import { getApmPackagePolicyDefinition } from './get_apm_package_policy_definition'; export async function createCloudApmPackgePolicy({ + cloudPluginSetup, fleetPluginStart, savedObjectsClient, esClient, logger, }: { + cloudPluginSetup: APMPluginSetupDependencies['cloud']; fleetPluginStart: NonNullable; savedObjectsClient: SavedObjectsClientContract; esClient: ElasticsearchClient; @@ -35,9 +40,10 @@ export async function createCloudApmPackgePolicy({ const apmServerSchema: Record = JSON.parse( (attributes as { schemaJson: string }).schemaJson ); - const apmPackagePolicyDefinition = getApmPackagePolicyDefinition( - apmServerSchema - ); + const apmPackagePolicyDefinition = getApmPackagePolicyDefinition({ + apmServerSchema, + cloudPluginSetup, + }); logger.info(`Fleet migration on Cloud - apmPackagePolicy create start`); const apmPackagePolicy = await fleetPluginStart.packagePolicyService.create( savedObjectsClient, diff --git a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts index fb88a092cb265d..82e85e7da9bb3d 100644 --- a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts +++ b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts @@ -5,16 +5,21 @@ * 2.0. */ +import { APMPluginSetupDependencies } from '../../types'; import { POLICY_ELASTIC_AGENT_ON_CLOUD, APM_PACKAGE_NAME, } from './get_cloud_apm_package_policy'; +interface GetApmPackagePolicyDefinitionOptions { + apmServerSchema: Record; + cloudPluginSetup: APMPluginSetupDependencies['cloud']; +} export function getApmPackagePolicyDefinition( - apmServerSchema: Record + options: GetApmPackagePolicyDefinitionOptions ) { return { - name: 'apm', + name: 'Elastic APM', namespace: 'default', enabled: true, policy_id: POLICY_ELASTIC_AGENT_ON_CLOUD, @@ -24,7 +29,7 @@ export function getApmPackagePolicyDefinition( type: 'apm', enabled: true, streams: [], - vars: getApmPackageInputVars(apmServerSchema), + vars: getApmPackageInputVars(options), }, ], package: { @@ -35,16 +40,17 @@ export function getApmPackagePolicyDefinition( }; } -function getApmPackageInputVars(apmServerSchema: Record) { +function getApmPackageInputVars(options: GetApmPackagePolicyDefinitionOptions) { + const { apmServerSchema } = options; const apmServerConfigs = Object.entries( apmConfigMapping - ).map(([key, { name, type }]) => ({ key, name, type })); + ).map(([key, { name, type, getValue }]) => ({ key, name, type, getValue })); const inputVars: Record< string, { type: string; value: any } - > = apmServerConfigs.reduce((acc, { key, name, type }) => { - const value = apmServerSchema[key] ?? ''; // defaults to an empty string to be edited in Fleet UI + > = apmServerConfigs.reduce((acc, { key, name, type, getValue }) => { + const value = (getValue ? getValue(options) : apmServerSchema[key]) ?? ''; // defaults to an empty string to be edited in Fleet UI return { ...acc, [name]: { type, value }, @@ -55,7 +61,11 @@ function getApmPackageInputVars(apmServerSchema: Record) { export const apmConfigMapping: Record< string, - { name: string; type: string } + { + name: string; + type: string; + getValue?: (options: GetApmPackagePolicyDefinitionOptions) => any; + } > = { 'apm-server.host': { name: 'host', @@ -64,6 +74,7 @@ export const apmConfigMapping: Record< 'apm-server.url': { name: 'url', type: 'text', + getValue: ({ cloudPluginSetup }) => cloudPluginSetup?.apm?.url, }, 'apm-server.secret_token': { name: 'secret_token', diff --git a/x-pack/plugins/apm/server/routes/fleet.ts b/x-pack/plugins/apm/server/routes/fleet.ts index 6208b42e844fa5..b83bfd54b93cd2 100644 --- a/x-pack/plugins/apm/server/routes/fleet.ts +++ b/x-pack/plugins/apm/server/routes/fleet.ts @@ -163,6 +163,7 @@ const createCloudApmPackagePolicyRoute = createApmServerRoute({ const coreStart = await resources.core.start(); const esClient = coreStart.elasticsearch.client.asScoped(resources.request) .asCurrentUser; + const cloudPluginSetup = plugins.cloud?.setup; const fleetPluginStart = await plugins.fleet.start(); const securityPluginStart = await plugins.security.start(); const hasRequiredRole = isSuperuser({ securityPluginStart, request }); @@ -171,6 +172,7 @@ const createCloudApmPackagePolicyRoute = createApmServerRoute({ } return { cloud_apm_package_policy: await createCloudApmPackgePolicy({ + cloudPluginSetup, fleetPluginStart, savedObjectsClient, esClient, From d5b1279182bfdc5a27bbb05eabb355fc8823f4e6 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 8 Jul 2021 15:48:03 -0400 Subject: [PATCH 22/77] [Fleet] Make Unhealthy status excluding updating agents (#104949) --- .../plugins/fleet/common/services/agent_status.ts | 2 +- .../fleet_api_integration/apis/fleet_telemetry.ts | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/fleet/common/services/agent_status.ts b/x-pack/plugins/fleet/common/services/agent_status.ts index b8a59e64477234..e4b227b79536ca 100644 --- a/x-pack/plugins/fleet/common/services/agent_status.ts +++ b/x-pack/plugins/fleet/common/services/agent_status.ts @@ -54,7 +54,7 @@ export function buildKueryForOnlineAgents() { } export function buildKueryForErrorAgents() { - return 'last_checkin_status:error or last_checkin_status:degraded'; + return `(last_checkin_status:error or last_checkin_status:degraded) AND not (${buildKueryForUpdatingAgents()})`; } export function buildKueryForOfflineAgents() { diff --git a/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts b/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts index 36eef019f7bf72..ed79d7200c4ede 100644 --- a/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts +++ b/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts @@ -30,6 +30,13 @@ export default function (providerContext: FtrProviderContext) { case 'offline': data = { last_checkin: '2017-06-07T18:59:04.498Z' }; break; + // Agent with last checkin status as error and currently unenrolling => should displayd updating status + case 'error-unenrolling': + data = { + last_checkin_status: 'error', + unenrollment_started_at: '2017-06-07T18:59:04.498Z', + }; + break; default: data = { last_checkin: new Date().toISOString() }; } @@ -95,6 +102,7 @@ export default function (providerContext: FtrProviderContext) { await generateAgent('offline', defaultServerPolicy.id); await generateAgent('error', defaultServerPolicy.id); await generateAgent('degraded', defaultServerPolicy.id); + await generateAgent('error-unenrolling', defaultServerPolicy.id); }); it('should return the correct telemetry values for fleet', async () => { @@ -109,12 +117,12 @@ export default function (providerContext: FtrProviderContext) { .expect(200); expect(apiResponse.stack_stats.kibana.plugins.fleet.agents).eql({ - total_enrolled: 7, + total_enrolled: 8, healthy: 3, unhealthy: 3, offline: 1, - updating: 0, - total_all_statuses: 7, + updating: 1, + total_all_statuses: 8, }); expect(apiResponse.stack_stats.kibana.plugins.fleet.fleet_server).eql({ From d41f2dc57e92636f2e69444ca7dbe94878de88bb Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 8 Jul 2021 15:33:25 -0500 Subject: [PATCH 23/77] [Fleet] Fix blank page when uninstalling outdated integration (#104938) * [Fleet] Fix blank page when uninstalling outdated integration * Make redirect conditional on version mismatch --- .../hooks/use_package_install.tsx | 15 +++++- .../detail/settings/installation_button.tsx | 7 +-- .../epm/screens/detail/settings/settings.tsx | 52 ++++++++++++++++++- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx index 342d6b54c2613d..edbe06f33b18e5 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx @@ -118,7 +118,12 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar ); const uninstallPackage = useCallback( - async ({ name, version, title }: Pick) => { + async ({ + name, + version, + title, + redirectToVersion, + }: Pick & { redirectToVersion: string }) => { setPackageInstallStatus({ name, status: InstallStatus.uninstalling, version }); const pkgkey = `${name}-${version}`; @@ -160,9 +165,15 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar /> ), }); + if (redirectToVersion !== version) { + const settingsPath = getPath('integration_details_settings', { + pkgkey: `${name}-${redirectToVersion}`, + }); + history.push(settingsPath); + } } }, - [notifications.toasts, setPackageInstallStatus] + [notifications.toasts, setPackageInstallStatus, getPath, history] ); return { diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/installation_button.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/installation_button.tsx index 8cf8466e6d9b0b..eab28a051f0611 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/installation_button.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/installation_button.tsx @@ -24,9 +24,10 @@ import { ConfirmPackageInstall } from './confirm_package_install'; type InstallationButtonProps = Pick & { disabled?: boolean; isUpdate?: boolean; + latestVersion?: string; }; export function InstallationButton(props: InstallationButtonProps) { - const { assets, name, title, version, disabled = true, isUpdate = false } = props; + const { assets, name, title, version, disabled = true, isUpdate = false, latestVersion } = props; const hasWriteCapabilites = useCapabilities().write; const installPackage = useInstallPackage(); const uninstallPackage = useUninstallPackage(); @@ -52,9 +53,9 @@ export function InstallationButton(props: InstallationButtonProps) { }, [installPackage, name, title, version]); const handleClickUninstall = useCallback(() => { - uninstallPackage({ name, version, title }); + uninstallPackage({ name, version, title, redirectToVersion: latestVersion ?? version }); toggleModal(); - }, [uninstallPackage, name, title, toggleModal, version]); + }, [uninstallPackage, name, title, toggleModal, version, latestVersion]); // counts the number of assets in the package const numOfAssets = useMemo( diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx index 9e8d200344b01d..14f378bc379a67 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx @@ -10,11 +10,11 @@ import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import semverLt from 'semver/functions/lt'; -import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLink } from '@elastic/eui'; import type { PackageInfo } from '../../../../../types'; import { InstallStatus } from '../../../../../types'; -import { useGetPackagePolicies, useGetPackageInstallStatus } from '../../../../../hooks'; +import { useGetPackagePolicies, useGetPackageInstallStatus, useLink } from '../../../../../hooks'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; import { UpdateIcon } from '../components'; @@ -47,6 +47,21 @@ const UpdatesAvailableMsg = () => ( ); +const LatestVersionLink = ({ name, version }: { name: string; version: string }) => { + const { getPath } = useLink(); + const settingsPath = getPath('integration_details_settings', { + pkgkey: `${name}-${version}`, + }); + return ( + + + + ); +}; + interface Props { packageInfo: PackageInfo; } @@ -72,6 +87,7 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => { (installationStatus === InstallStatus.installed && installedVersion !== version); const isUpdating = installationStatus === InstallStatus.installing && installedVersion; + return ( @@ -206,6 +222,7 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => {

@@ -244,6 +261,37 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => { )} )} + {hideInstallOptions && isViewingOldPackage && !isUpdating && ( +
+ +
+ +

+ +

+
+ +

+ + , + }} + /> + +

+
+
+ )}
From 149d3c5685d7d39cc38b31a75111b56393a6bd1a Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 8 Jul 2021 19:29:25 -0400 Subject: [PATCH 24/77] [Security Solution][Endpoint] Fix permission checks for endpoint management funcionality (#104555) * refactor: move `UserPrivilegesContext` to `public/common` * Add endpointPrivileges to the UserPrivileges context * refactor `useUpgradeSecurityPackages` to use `useUserPrivileges()` * Refactor to use `useUserPrivileges()` instead of `useIngestEnabledCheck()` * Refactor Overview page to use `useUserPrivileges()` instead of `useIngestEnabledCheck()` * Delete `ingest_enabled` hook and refactor tests --- .../security_solution/public/app/app.tsx | 2 +- .../__mocks__/use_endpoint_privileges.ts | 17 +++ .../components/user_privileges/index.tsx | 17 ++- .../use_endpoint_privileges.test.ts | 120 ++++++++++++++++++ .../use_endpoint_privileges.ts | 77 +++++++++++ .../common/hooks/endpoint/ingest_enabled.ts | 35 ----- .../hooks/use_upgrade_security_packages.ts | 41 +----- .../public/common/mock/test_providers.tsx | 2 +- .../use_missing_privileges.ts | 2 +- .../components/user_info/index.test.tsx | 3 +- .../alerts/use_alerts_privileges.test.tsx | 5 +- .../alerts/use_alerts_privileges.tsx | 2 +- .../alerts/use_signal_index.test.tsx | 1 + .../lists/use_lists_privileges.tsx | 2 +- .../management/pages/endpoint_hosts/mocks.ts | 81 ++++++++---- .../public/management/pages/index.test.tsx | 18 ++- .../public/management/pages/index.tsx | 13 +- .../components/overview_empty/index.test.tsx | 14 +- .../components/overview_empty/index.tsx | 6 +- .../public/overview/pages/overview.test.tsx | 55 ++++++-- .../public/overview/pages/overview.tsx | 7 +- 21 files changed, 378 insertions(+), 142 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts rename x-pack/plugins/security_solution/public/{detections => common}/components/user_privileges/index.tsx (59%) create mode 100644 x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts delete mode 100644 x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx index c223570c77201c..0cba9341cbce18 100644 --- a/x-pack/plugins/security_solution/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -24,7 +24,7 @@ import { State } from '../common/store'; import { StartServices } from '../types'; import { PageRouter } from './routes'; import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; -import { UserPrivilegesProvider } from '../detections/components/user_privileges'; +import { UserPrivilegesProvider } from '../common/components/user_privileges'; interface StartAppComponent { children: React.ReactNode; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts new file mode 100644 index 00000000000000..80cf11fecd847a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EndpointPrivileges } from '../use_endpoint_privileges'; + +export const useEndpointPrivileges = jest.fn(() => { + const endpointPrivilegesMock: EndpointPrivileges = { + loading: false, + canAccessFleet: true, + canAccessEndpointManagement: true, + }; + return endpointPrivilegesMock; +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/user_privileges/index.tsx b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx similarity index 59% rename from x-pack/plugins/security_solution/public/detections/components/user_privileges/index.tsx rename to x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx index bd6ff11b27f961..5a33297f04f9a4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_privileges/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx @@ -6,19 +6,25 @@ */ import React, { createContext, useContext } from 'react'; -import { useFetchDetectionEnginePrivileges } from './use_fetch_detection_engine_privileges'; -import { useFetchListPrivileges } from './use_fetch_list_privileges'; +import { DeepReadonly } from 'utility-types'; +import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges'; +import { useFetchListPrivileges } from '../../../detections/components/user_privileges/use_fetch_list_privileges'; +import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges'; export interface UserPrivilegesState { listPrivileges: ReturnType; detectionEnginePrivileges: ReturnType; + endpointPrivileges: EndpointPrivileges; } -const UserPrivilegesContext = createContext({ +export const initialUserPrivilegesState = (): UserPrivilegesState => ({ listPrivileges: { loading: false, error: undefined, result: undefined }, detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, + endpointPrivileges: { loading: true, canAccessEndpointManagement: false, canAccessFleet: false }, }); +const UserPrivilegesContext = createContext(initialUserPrivilegesState()); + interface UserPrivilegesProviderProps { children: React.ReactNode; } @@ -26,12 +32,14 @@ interface UserPrivilegesProviderProps { export const UserPrivilegesProvider = ({ children }: UserPrivilegesProviderProps) => { const listPrivileges = useFetchListPrivileges(); const detectionEnginePrivileges = useFetchDetectionEnginePrivileges(); + const endpointPrivileges = useEndpointPrivileges(); return ( {children} @@ -39,4 +47,5 @@ export const UserPrivilegesProvider = ({ children }: UserPrivilegesProviderProps ); }; -export const useUserPrivileges = () => useContext(UserPrivilegesContext); +export const useUserPrivileges = (): DeepReadonly => + useContext(UserPrivilegesContext); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts new file mode 100644 index 00000000000000..8e9dae9f12ad57 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, RenderHookResult, RenderResult } from '@testing-library/react-hooks'; +import { useHttp, useCurrentUser } from '../../lib/kibana'; +import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges'; +import { fleetGetCheckPermissionsHttpMock } from '../../../management/pages/endpoint_hosts/mocks'; +import { securityMock } from '../../../../../security/public/mocks'; +import { appRoutesService } from '../../../../../fleet/common'; +import { AuthenticatedUser } from '../../../../../security/common'; + +jest.mock('../../lib/kibana'); + +describe('When using useEndpointPrivileges hook', () => { + let authenticatedUser: AuthenticatedUser; + let fleetApiMock: ReturnType; + let result: RenderResult; + let unmount: ReturnType['unmount']; + let waitForNextUpdate: ReturnType['waitForNextUpdate']; + let render: () => RenderHookResult; + + beforeEach(() => { + authenticatedUser = securityMock.createMockAuthenticatedUser({ + roles: ['superuser'], + }); + + (useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser); + + fleetApiMock = fleetGetCheckPermissionsHttpMock( + useHttp() as Parameters[0] + ); + + render = () => { + const hookRenderResponse = renderHook(() => useEndpointPrivileges()); + ({ result, unmount, waitForNextUpdate } = hookRenderResponse); + return hookRenderResponse; + }; + }); + + afterEach(() => { + unmount(); + }); + + it('should return `loading: true` while retrieving privileges', async () => { + // Add a daly to the API response that we can control from the test + let releaseApiResponse: () => void; + fleetApiMock.responseProvider.checkPermissions.mockDelay.mockReturnValue( + new Promise((resolve) => { + releaseApiResponse = () => resolve(); + }) + ); + (useCurrentUser as jest.Mock).mockReturnValue(null); + + const { rerender } = render(); + expect(result.current).toEqual({ + canAccessEndpointManagement: false, + canAccessFleet: false, + loading: true, + }); + + // Make user service available + (useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser); + rerender(); + expect(result.current).toEqual({ + canAccessEndpointManagement: false, + canAccessFleet: false, + loading: true, + }); + + // Release the API response + releaseApiResponse!(); + await fleetApiMock.waitForApi(); + expect(result.current).toEqual({ + canAccessEndpointManagement: true, + canAccessFleet: true, + loading: false, + }); + }); + + it('should call Fleet permissions api to determine user privilege to fleet', async () => { + render(); + await waitForNextUpdate(); + await fleetApiMock.waitForApi(); + expect(useHttp().get as jest.Mock).toHaveBeenCalledWith( + appRoutesService.getCheckPermissionsPath() + ); + }); + + it('should set privileges to false if user does not have superuser role', async () => { + authenticatedUser.roles = []; + render(); + await waitForNextUpdate(); + await fleetApiMock.waitForApi(); + expect(result.current).toEqual({ + canAccessEndpointManagement: false, + canAccessFleet: true, // this is only true here because I did not adjust the API mock + loading: false, + }); + }); + + it('should set privileges to false if fleet api check returns failure', async () => { + fleetApiMock.responseProvider.checkPermissions.mockReturnValue({ + error: 'MISSING_SECURITY', + success: false, + }); + + render(); + await waitForNextUpdate(); + await fleetApiMock.waitForApi(); + expect(result.current).toEqual({ + canAccessEndpointManagement: false, + canAccessFleet: false, + loading: false, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts new file mode 100644 index 00000000000000..b8db0c5c0fbc9e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useCurrentUser, useHttp } from '../../lib/kibana'; +import { appRoutesService, CheckPermissionsResponse } from '../../../../../fleet/common'; + +export interface EndpointPrivileges { + loading: boolean; + /** If user has permissions to access Fleet */ + canAccessFleet: boolean; + /** If user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */ + canAccessEndpointManagement: boolean; +} + +/** + * Retrieve the endpoint privileges for the current user. + * + * **NOTE:** Consider using `usePrivileges().endpointPrivileges` instead of this hook in order + * to keep API calls to a minimum. + */ +export const useEndpointPrivileges = (): EndpointPrivileges => { + const http = useHttp(); + const user = useCurrentUser(); + const isMounted = useRef(true); + const [canAccessFleet, setCanAccessFleet] = useState(false); + const [fleetCheckDone, setFleetCheckDone] = useState(false); + + // Check if user can access fleet + useEffect(() => { + (async () => { + try { + const fleetPermissionsResponse = await http.get( + appRoutesService.getCheckPermissionsPath() + ); + + if (isMounted.current) { + setCanAccessFleet(fleetPermissionsResponse.success); + } + } finally { + if (isMounted.current) { + setFleetCheckDone(true); + } + } + })(); + }, [http]); + + // Check if user has `superuser` role + const isSuperUser = useMemo(() => { + if (user?.roles) { + return user.roles.includes('superuser'); + } + return false; + }, [user?.roles]); + + const privileges = useMemo(() => { + return { + loading: !fleetCheckDone || !user, + canAccessFleet, + canAccessEndpointManagement: canAccessFleet && isSuperUser, + }; + }, [canAccessFleet, fleetCheckDone, isSuperUser, user]); + + // Capture if component is unmounted + useEffect( + () => () => { + isMounted.current = false; + }, + [] + ); + + return privileges; +}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts deleted file mode 100644 index 18582e7064a7b9..00000000000000 --- a/x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ApplicationStart } from 'src/core/public'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -/** - * Returns an object which fleet permissions are allowed - */ -export const useIngestEnabledCheck = (): { - allEnabled: boolean; - show: boolean; - write: boolean; - read: boolean; -} => { - const { services } = useKibana<{ application: ApplicationStart }>(); - - // Check if Fleet is present in the configuration - const show = Boolean(services.application.capabilities.fleet?.show); - const write = Boolean(services.application.capabilities.fleet?.write); - const read = Boolean(services.application.capabilities.fleet?.read); - - // Check if all Fleet permissions are enabled - const allEnabled = show && read && write ? true : false; - - return { - allEnabled, - show, - write, - read, - }; -}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts index 6a3afccd8794db..ef1e658d349bfb 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts @@ -8,14 +8,9 @@ import { useEffect } from 'react'; import { HttpFetchOptions, HttpStart } from 'kibana/public'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { - epmRouteService, - appRoutesService, - CheckPermissionsResponse, - BulkInstallPackagesResponse, -} from '../../../../fleet/common'; +import { epmRouteService, BulkInstallPackagesResponse } from '../../../../fleet/common'; import { StartServices } from '../../types'; -import { useIngestEnabledCheck } from './endpoint/ingest_enabled'; +import { useUserPrivileges } from '../components/user_privileges'; /** * Requests that the endpoint and security_detection_engine package be upgraded to the latest version @@ -35,25 +30,9 @@ const sendUpgradeSecurityPackages = async ( }); }; -/** - * Checks with the ingest manager if the current user making these requests has the right permissions - * to install the endpoint package. - * - * @param http an http client for sending the request - * @param options an object containing options for the request - */ -const sendCheckPermissions = async ( - http: HttpStart, - options: HttpFetchOptions = {} -): Promise => { - return http.get(appRoutesService.getCheckPermissionsPath(), { - ...options, - }); -}; - export const useUpgradeSecurityPackages = () => { const context = useKibana(); - const { allEnabled: ingestEnabled } = useIngestEnabledCheck(); + const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet; useEffect(() => { const abortController = new AbortController(); @@ -63,21 +42,11 @@ export const useUpgradeSecurityPackages = () => { abortController.abort(); }; - if (ingestEnabled) { + if (canAccessFleet) { const signal = abortController.signal; (async () => { try { - // make sure we're a privileged user before trying to install the package - const { success: hasPermissions } = await sendCheckPermissions(context.services.http, { - signal, - }); - - // if we're not a privileged user then return and don't try to check the status of the endpoint package - if (!hasPermissions) { - return abortRequests; - } - // ignore the response for now since we aren't notifying the user await sendUpgradeSecurityPackages(context.services.http, { signal }); } catch (error) { @@ -93,5 +62,5 @@ export const useUpgradeSecurityPackages = () => { return abortRequests; })(); } - }, [ingestEnabled, context.services.http]); + }, [canAccessFleet, context.services.http]); }; diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 9ac7ae0f243225..d0755d05bdb5ff 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -24,7 +24,7 @@ import { import { FieldHook } from '../../shared_imports'; import { SUB_PLUGINS_REDUCER } from './utils'; import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage'; -import { UserPrivilegesProvider } from '../../detections/components/user_privileges'; +import { UserPrivilegesProvider } from '../components/user_privileges'; const state: State = mockGlobalState; diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/use_missing_privileges.ts b/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/use_missing_privileges.ts index dd139421e9ddde..73aa922251ee62 100644 --- a/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/use_missing_privileges.ts +++ b/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/use_missing_privileges.ts @@ -9,7 +9,7 @@ import { useMemo } from 'react'; import { SAVED_OBJECTS_MANAGEMENT_FEATURE_ID } from '../../../../../common/constants'; import { Privilege } from '../../../containers/detection_engine/alerts/types'; import { useUserData } from '../../user_info'; -import { useUserPrivileges } from '../../user_privileges'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; const REQUIRED_INDEX_PRIVILIGES = ['read', 'write', 'view_index_metadata', 'maintenance'] as const; diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx index 83ca0026b89342..bb9ec01399f8d1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx @@ -12,10 +12,11 @@ import { useUserInfo, ManageUserInfo } from './index'; import { useKibana } from '../../../common/lib/kibana'; import * as api from '../../containers/detection_engine/alerts/api'; import { TestProviders } from '../../../common/mock/test_providers'; -import { UserPrivilegesProvider } from '../user_privileges'; +import { UserPrivilegesProvider } from '../../../common/components/user_privileges'; jest.mock('../../../common/lib/kibana'); jest.mock('../../containers/detection_engine/alerts/api'); +jest.mock('../../../common/components/user_privileges/use_endpoint_privileges'); describe('useUserInfo', () => { beforeAll(() => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx index 651d80f3165abb..f3afe833652866 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx @@ -9,13 +9,13 @@ import { act, renderHook } from '@testing-library/react-hooks'; import produce from 'immer'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; -import { useUserPrivileges } from '../../../components/user_privileges'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { Privilege } from './types'; import { UseAlertsPrivelegesReturn, useAlertsPrivileges } from './use_alerts_privileges'; jest.mock('./api'); jest.mock('../../../../common/hooks/use_app_toasts'); -jest.mock('../../../components/user_privileges'); +jest.mock('../../../../common/components/user_privileges'); const useUserPrivilegesMock = useUserPrivileges as jest.Mock>; @@ -86,6 +86,7 @@ const userPrivilegesInitial: ReturnType = { result: undefined, error: undefined, }, + endpointPrivileges: { loading: true, canAccessEndpointManagement: false, canAccessFleet: false }, }; describe('usePrivilegeUser', () => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx index 08e28521e14730..005224a80c1891 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx @@ -6,7 +6,7 @@ */ import { useEffect, useState } from 'react'; -import { useUserPrivileges } from '../../../components/user_privileges'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; export interface UseAlertsPrivelegesReturn extends AlertsPrivelegesState { loading: boolean; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx index ce262ce4f9a2e8..ade83fed4fd6bb 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx @@ -13,6 +13,7 @@ import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; jest.mock('./api'); jest.mock('../../../../common/hooks/use_app_toasts'); +jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); describe('useSignalIndex', () => { let appToastsMock: jest.Mocked>; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx index 0b7cd673c49f44..5f21f0287d7eae 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx @@ -6,7 +6,7 @@ */ import { useEffect, useState } from 'react'; -import { useUserPrivileges } from '../../../components/user_privileges'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { Privilege } from '../alerts/types'; export interface UseListsPrivilegesState { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts index 5498d139fd6e1b..d6b24fa3cbdfc2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts @@ -25,6 +25,8 @@ import { } from '../../../../common/endpoint/constants'; import { AGENT_POLICY_API_ROUTES, + appRoutesService, + CheckPermissionsResponse, EPM_API_ROUTES, GetAgentPoliciesResponse, GetPackagesResponse, @@ -122,37 +124,66 @@ export const fleetGetPackageListHttpMock = httpHandlerMockFactory GetAgentPoliciesResponse; }>; -export const fleetGetAgentPolicyListHttpMock = httpHandlerMockFactory([ - { - id: 'agentPolicy', - path: AGENT_POLICY_API_ROUTES.LIST_PATTERN, - method: 'get', - handler: () => { - const generator = new EndpointDocGenerator('seed'); - const endpointMetadata = generator.generateHostMetadata(); - const agentPolicy = generator.generateAgentPolicy(); - - // Make sure that the Agent policy returned from the API has the Integration Policy ID that - // the endpoint metadata is using. This is needed especially when testing the Endpoint Details - // flyout where certain actions might be disabled if we know the endpoint integration policy no - // longer exists. - (agentPolicy.package_policies as string[]).push(endpointMetadata.Endpoint.policy.applied.id); - - return { - items: [agentPolicy], - perPage: 10, - total: 1, - page: 1, - }; +export const fleetGetAgentPolicyListHttpMock = httpHandlerMockFactory( + [ + { + id: 'agentPolicy', + path: AGENT_POLICY_API_ROUTES.LIST_PATTERN, + method: 'get', + handler: () => { + const generator = new EndpointDocGenerator('seed'); + const endpointMetadata = generator.generateHostMetadata(); + const agentPolicy = generator.generateAgentPolicy(); + + // Make sure that the Agent policy returned from the API has the Integration Policy ID that + // the endpoint metadata is using. This is needed especially when testing the Endpoint Details + // flyout where certain actions might be disabled if we know the endpoint integration policy no + // longer exists. + (agentPolicy.package_policies as string[]).push( + endpointMetadata.Endpoint.policy.applied.id + ); + + return { + items: [agentPolicy], + perPage: 10, + total: 1, + page: 1, + }; + }, }, - }, -]); + ] +); + +export type FleetGetCheckPermissionsInterface = ResponseProvidersInterface<{ + checkPermissions: () => CheckPermissionsResponse; +}>; + +export const fleetGetCheckPermissionsHttpMock = httpHandlerMockFactory( + [ + { + id: 'checkPermissions', + path: appRoutesService.getCheckPermissionsPath(), + method: 'get', + handler: () => { + return { + error: undefined, + success: true, + }; + }, + }, + ] +); type FleetApisHttpMockInterface = FleetGetPackageListHttpMockInterface & - FleetGetAgentPolicyListHttpMockInterface; + FleetGetAgentPolicyListHttpMockInterface & + FleetGetCheckPermissionsInterface; +/** + * Mocks all Fleet apis needed to render the Endpoint List/Details pages + */ export const fleetApisHttpMock = composeHttpHandlerMocks([ fleetGetPackageListHttpMock, fleetGetAgentPolicyListHttpMock, + fleetGetCheckPermissionsHttpMock, ]); type EndpointPageHttpMockInterface = EndpointMetadataHttpMocksInterface & diff --git a/x-pack/plugins/security_solution/public/management/pages/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/index.test.tsx index 9f2ed3618b06d7..821e14edfda451 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.test.tsx @@ -10,11 +10,11 @@ import React from 'react'; import { ManagementContainer } from './index'; import '../../common/mock/match_media.ts'; import { AppContextTestRender, createAppRootMockRenderer } from '../../common/mock/endpoint'; -import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; +import { useUserPrivileges } from '../../common/components/user_privileges'; -jest.mock('../../common/hooks/endpoint/ingest_enabled'); +jest.mock('../../common/components/user_privileges'); -describe('when in the Admistration tab', () => { +describe('when in the Administration tab', () => { let render: () => ReturnType; beforeEach(() => { @@ -23,14 +23,18 @@ describe('when in the Admistration tab', () => { mockedContext.history.push('/administration/endpoints'); }); - it('should display the No Permissions view when Ingest is OFF', async () => { - (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: false }); + it('should display the No Permissions if no sufficient privileges', async () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + endpointPrivileges: { loading: false, canAccessEndpointManagement: false }, + }); expect(await render().findByTestId('noIngestPermissions')).not.toBeNull(); }); - it('should display the Management view when Ingest is ON', async () => { - (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); + it('should display the Management view if user has privileges', async () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + endpointPrivileges: { loading: false, canAccessEndpointManagement: true }, + }); expect(await render().findByTestId('endpointPage')).not.toBeNull(); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index 327dcd4458eeb8..f348be6089923b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -7,7 +7,7 @@ import React, { memo } from 'react'; import { Route, Switch, Redirect } from 'react-router-dom'; -import { EuiEmptyPrompt, EuiText } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { MANAGEMENT_ROUTING_ENDPOINTS_PATH, @@ -22,9 +22,9 @@ import { PolicyContainer } from './policy'; import { TrustedAppsContainer } from './trusted_apps'; import { MANAGEMENT_PATH, SecurityPageName } from '../../../common/constants'; import { SpyRoute } from '../../common/utils/route/spy_routes'; -import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; import { EventFiltersContainer } from './event_filters'; import { getEndpointListPath } from '../common/routing'; +import { useUserPrivileges } from '../../common/components/user_privileges'; const NoPermissions = memo(() => { return ( @@ -80,9 +80,14 @@ const EventFilterTelemetry = () => ( ); export const ManagementContainer = memo(() => { - const { allEnabled: isIngestEnabled } = useIngestEnabledCheck(); + const { loading, canAccessEndpointManagement } = useUserPrivileges().endpointPrivileges; - if (!isIngestEnabled) { + // Lets wait until we can verify permissions + if (loading) { + return ; + } + + if (!canAccessEndpointManagement) { return ; } diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx index f0198092ec1be8..7af9f84ad08757 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; import { OverviewEmpty } from '.'; -import { useIngestEnabledCheck } from '../../../common/hooks/endpoint/ingest_enabled'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; const endpointPackageVersion = '0.19.1'; @@ -20,8 +20,10 @@ jest.mock('../../../management/pages/endpoint_hosts/view/hooks', () => ({ useEndpointSelector: jest.fn().mockReturnValue({ endpointPackageVersion }), })); -jest.mock('../../../common/hooks/endpoint/ingest_enabled', () => ({ - useIngestEnabledCheck: jest.fn().mockReturnValue({ allEnabled: true }), +jest.mock('../../../common/components/user_privileges', () => ({ + useUserPrivileges: jest + .fn() + .mockReturnValue({ endpointPrivileges: { loading: false, canAccessFleet: true } }), })); jest.mock('../../../common/hooks/endpoint/use_navigate_to_app_event_handler', () => ({ @@ -36,7 +38,7 @@ describe('OverviewEmpty', () => { }); afterAll(() => { - (useIngestEnabledCheck as jest.Mock).mockReset(); + (useUserPrivileges as jest.Mock).mockReset(); }); test('render with correct actions ', () => { @@ -70,7 +72,9 @@ describe('OverviewEmpty', () => { describe('When isIngestEnabled = false', () => { let wrapper: ShallowWrapper; beforeAll(() => { - (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: false }); + (useUserPrivileges as jest.Mock).mockReturnValue({ + endpointPrivileges: { loading: false, canAccessFleet: false }, + }); wrapper = shallow(); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx index 028871d7be19d8..c75438e18f5d58 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx @@ -20,9 +20,9 @@ import { useIngestUrl, } from '../../../management/pages/endpoint_hosts/view/hooks'; import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; -import { useIngestEnabledCheck } from '../../../common/hooks/endpoint/ingest_enabled'; import { CreateStructuredSelector } from '../../../common/store'; import { endpointPackageVersion as useEndpointPackageVersion } from '../../../management/pages/endpoint_hosts/store/selectors'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; const OverviewEmptyComponent: React.FC = () => { const { http, docLinks } = useKibana().services; @@ -40,7 +40,7 @@ const OverviewEmptyComponent: React.FC = () => { const handleEndpointClick = useNavigateToAppEventHandler('fleet', { path: endpointIntegrationUrl, }); - const { allEnabled: isIngestEnabled } = useIngestEnabledCheck(); + const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet; const emptyPageActions: EmptyPageActionsProps = useMemo( () => ({ @@ -72,7 +72,7 @@ const OverviewEmptyComponent: React.FC = () => { [emptyPageActions] ); - return isIngestEnabled === true ? ( + return canAccessFleet === true ? ( ({ jest.mock('../../common/components/query_bar', () => ({ QueryBar: () => null, })); -jest.mock('../../common/hooks/endpoint/ingest_enabled'); +jest.mock('../../common/components/user_privileges', () => { + return { + ...jest.requireActual('../../common/components/user_privileges'), + useUserPrivileges: jest.fn(() => { + return { + listPrivileges: { loading: false, error: undefined, result: undefined }, + detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, + endpointPrivileges: { + loading: false, + canAccessEndpointManagement: true, + canAccessFleet: true, + }, + }; + }), + }; +}); jest.mock('../../common/containers/local_storage/use_messages_storage'); jest.mock('../containers/overview_cti_links'); jest.mock('../containers/overview_cti_links/use_cti_event_counts'); jest.mock('../containers/overview_cti_links'); + const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; useCtiDashboardLinksMock.mockReturnValue(mockCtiLinksResponse); @@ -74,12 +95,25 @@ const endpointNoticeMessage = (hasMessageValue: boolean) => { }; }; const mockUseSourcererScope = useSourcererScope as jest.Mock; -const mockUseIngestEnabledCheck = useIngestEnabledCheck as jest.Mock; +const mockUseUserPrivileges = useUserPrivileges as jest.Mock; const mockUseFetchIndex = useFetchIndex as jest.Mock; const mockUseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; describe('Overview', () => { + const loadedUserPrivilegesState = ( + endpointOverrides: Partial = {} + ): ReturnType => + merge(initialUserPrivilegesState(), { + endpointPrivileges: { + loading: false, + canAccessFleet: true, + canAccessEndpointManagement: true, + ...endpointOverrides, + }, + }); + beforeEach(() => { + mockUseUserPrivileges.mockReturnValue(loadedUserPrivilegesState()); mockUseFetchIndex.mockReturnValue([ false, { @@ -88,6 +122,10 @@ describe('Overview', () => { ]); }); + afterAll(() => { + mockUseUserPrivileges.mockReset(); + }); + describe('rendering', () => { test('it DOES NOT render the Getting started text when an index is available', () => { mockUseSourcererScope.mockReturnValue({ @@ -97,7 +135,6 @@ describe('Overview', () => { }); mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); - mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -125,7 +162,6 @@ describe('Overview', () => { }); mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); - mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -153,7 +189,6 @@ describe('Overview', () => { }); mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); - mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -175,7 +210,6 @@ describe('Overview', () => { }); mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); - mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -197,7 +231,6 @@ describe('Overview', () => { }); mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); - mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -219,7 +252,7 @@ describe('Overview', () => { }); mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); - mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: false }); + mockUseUserPrivileges.mockReturnValue(loadedUserPrivilegesState({ canAccessFleet: false })); const wrapper = mount( @@ -239,7 +272,7 @@ describe('Overview', () => { selectedPatterns: [], indicesExist: false, }); - mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: false }); + mockUseUserPrivileges.mockReturnValue(loadedUserPrivilegesState({ canAccessFleet: false })); mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); }); @@ -266,7 +299,7 @@ describe('Overview', () => { }); it('shows Endpoint get ready button when ingest is enabled', () => { - mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); + mockUseUserPrivileges.mockReturnValue(loadedUserPrivilegesState({ canAccessFleet: true })); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 174141db9bfb19..ed12dce6db482f 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -27,13 +27,13 @@ import { SecurityPageName } from '../../app/types'; import { EndpointNotice } from '../components/endpoint_notice'; import { useMessagesStorage } from '../../common/containers/local_storage/use_messages_storage'; import { ENDPOINT_METADATA_INDEX } from '../../../common/constants'; -import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; import { useSourcererScope } from '../../common/containers/sourcerer'; import { Sourcerer } from '../../common/components/sourcerer'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { ThreatIntelLinkPanel } from '../components/overview_cti_links'; import { useIsThreatIntelModuleEnabled } from '../containers/overview_cti_links/use_is_threat_intel_module_enabled'; +import { useUserPrivileges } from '../../common/components/user_privileges'; const SidebarFlexItem = styled(EuiFlexItem)` margin-right: 24px; @@ -70,9 +70,8 @@ const OverviewComponent = () => { setDismissMessage(true); addMessage('management', 'dismissEndpointNotice'); }, [addMessage]); - const { allEnabled: isIngestEnabled } = useIngestEnabledCheck(); + const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet; const isThreatIntelModuleEnabled = useIsThreatIntelModuleEnabled(); - return ( <> {indicesExist ? ( @@ -82,7 +81,7 @@ const OverviewComponent = () => { - {!dismissMessage && !metadataIndexExists && isIngestEnabled && ( + {!dismissMessage && !metadataIndexExists && canAccessFleet && ( <> From 151dafc2f2ad58cd1d77913a3cbd5154b6be7975 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 8 Jul 2021 16:29:40 -0700 Subject: [PATCH 25/77] [Security Solution][Exceptions] - update exception lists page title and exceptions navigation title (#104803) ### Summary Updates exception lists page title and exceptions navigation title to "Exceptions" --- .../security_solution/public/app/deep_links/index.ts | 2 +- .../plugins/security_solution/public/app/translations.ts | 2 +- .../use_security_solution_navigation/index.test.tsx | 2 +- .../rules/all/exceptions/exceptions_table.tsx | 3 ++- .../rules/all/exceptions/translations.ts | 9 ++++++++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index e1c14f2a863803..f5cec592c7abf6 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -165,7 +165,7 @@ const nestedDeepLinks: SecurityDeepLinks = { navLinkStatus: AppNavLinkStatus.hidden, keywords: [ i18n.translate('xpack.securitySolution.search.exceptions', { - defaultMessage: 'Exception list', + defaultMessage: 'Exceptions', }), ], searchable: true, diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index 847a9114d94bd4..027789713a2ae7 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -24,7 +24,7 @@ export const RULES = i18n.translate('xpack.securitySolution.navigation.rules', { }); export const EXCEPTIONS = i18n.translate('xpack.securitySolution.navigation.exceptions', { - defaultMessage: 'Exception list', + defaultMessage: 'Exceptions', }); export const ALERTS = i18n.translate('xpack.securitySolution.navigation.alerts', { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx index e3549aa6ec0478..af88aacb7602af 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -141,7 +141,7 @@ describe('useSecuritySolutionNavigation', () => { "href": "securitySolution/exceptions?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "exceptions", "isSelected": false, - "name": "Exception list", + "name": "Exceptions", "onClick": [Function], }, ], diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index b4f5efe2348bb6..206976e6c0c1ac 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -336,7 +336,8 @@ export const ExceptionListsTable = React.memo(() => { <> <> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts index 0dd016425f4e65..912f5bec4de35c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts @@ -73,7 +73,14 @@ export const EXCEPTIONS_LISTS_SEARCH_PLACEHOLDER = i18n.translate( export const ALL_EXCEPTIONS = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allExceptions.tableTitle', { - defaultMessage: 'Exception Lists', + defaultMessage: 'Exceptions', + } +); + +export const ALL_EXCEPTIONS_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allExceptions.tableTitleDescription', + { + defaultMessage: 'Exceptions are automatically grouped into exception lists.', } ); From 6d9d1db6ace763fb4336c16d7856d6ee14573186 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 8 Jul 2021 16:30:00 -0700 Subject: [PATCH 26/77] [Security Solution] - Updates breadcrumbs text for returning to rules page (#104805) ### Summary Updates breadcrumbs text for returning to rules page from rule creation and rule details. --- .../pages/detection_engine/rules/create/translations.ts | 4 ++-- .../pages/detection_engine/rules/details/translations.ts | 4 ++-- x-pack/plugins/translations/translations/ja-JP.json | 2 -- x-pack/plugins/translations/translations/zh-CN.json | 2 -- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/translations.ts index 5a49ab349c0945..f4292f6c663cce 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/translations.ts @@ -15,9 +15,9 @@ export const PAGE_TITLE = i18n.translate( ); export const BACK_TO_RULES = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.backToRulesDescription', + 'xpack.securitySolution.detectionEngine.createRule.backToRulesButton', { - defaultMessage: 'Back to detection rules', + defaultMessage: 'Rules', } ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts index e42fde569bd670..ca3e5a4587a096 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts @@ -15,9 +15,9 @@ export const PAGE_TITLE = i18n.translate( ); export const BACK_TO_RULES = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDetails.backToRulesDescription', + 'xpack.securitySolution.detectionEngine.ruleDetails.backToRulesButton', { - defaultMessage: 'Back to detection rules', + defaultMessage: 'Rules', } ); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b21d4afbe00205..3809d55ff90bea 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19002,7 +19002,6 @@ "xpack.securitySolution.detectionEngine.components.importRuleModal.selectRuleDescription": "インポートするセキュリティルール (検出エンジンビューからエクスポートしたもの) を選択します", "xpack.securitySolution.detectionEngine.createRule. stepScheduleRule.completeWithActivatingTitle": "ルールの作成と有効化", "xpack.securitySolution.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle": "有効化せずにルールを作成", - "xpack.securitySolution.detectionEngine.createRule.backToRulesDescription": "検出ルールに戻る", "xpack.securitySolution.detectionEngine.createRule.editRuleButton": "編集", "xpack.securitySolution.detectionEngine.createRule.eqlRuleTypeDescription": "イベント相関関係", "xpack.securitySolution.detectionEngine.createRule.filtersLabel": "フィルター", @@ -19744,7 +19743,6 @@ "xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAggregatedByDescription": "結果集約条件", "xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAllDescription": "すべての結果", "xpack.securitySolution.detectionEngine.ruleDetails.activatedRuleLabel": "有効化", - "xpack.securitySolution.detectionEngine.ruleDetails.backToRulesDescription": "検出ルールに戻る", "xpack.securitySolution.detectionEngine.ruleDetails.errorCalloutTitle": "ルール失敗", "xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab": "例外", "xpack.securitySolution.detectionEngine.ruleDetails.experimentalDescription": "実験的", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 27a5a06f9ca7c8..6540d9f49399aa 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -19281,7 +19281,6 @@ "xpack.securitySolution.detectionEngine.components.importRuleModal.successfullyImportedRulesTitle": "已成功导入 {totalRules} 个{totalRules, plural, other {规则}}", "xpack.securitySolution.detectionEngine.createRule. stepScheduleRule.completeWithActivatingTitle": "创建并激活规则", "xpack.securitySolution.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle": "创建规则但不激活", - "xpack.securitySolution.detectionEngine.createRule.backToRulesDescription": "返回到检测规则", "xpack.securitySolution.detectionEngine.createRule.editRuleButton": "编辑", "xpack.securitySolution.detectionEngine.createRule.eqlRuleTypeDescription": "事件关联", "xpack.securitySolution.detectionEngine.createRule.filtersLabel": "筛选", @@ -20026,7 +20025,6 @@ "xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAggregatedByDescription": "结果聚合依据", "xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAllDescription": "所有结果", "xpack.securitySolution.detectionEngine.ruleDetails.activatedRuleLabel": "已激活", - "xpack.securitySolution.detectionEngine.ruleDetails.backToRulesDescription": "返回到检测规则", "xpack.securitySolution.detectionEngine.ruleDetails.errorCalloutTitle": "规则错误位置", "xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab": "例外", "xpack.securitySolution.detectionEngine.ruleDetails.experimentalDescription": "实验性", From fe6eb09936e671e1ecc80c5018bf97e2c412b1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Fri, 9 Jul 2021 02:39:46 +0300 Subject: [PATCH 27/77] [Osquery] Fix 7.14 UX issues (#104257) --- package.json | 2 +- .../action_results/action_agents_status.tsx | 91 +++++++++++ .../action_agents_status_badges.tsx | 50 ++++++ .../action_agents_status_bar.tsx | 46 ++++++ .../action_results/action_results_summary.tsx | 126 ++++----------- .../action_results/services/agent_status.tsx | 59 +++++++ .../osquery/public/action_results/types.ts | 8 + .../action_results/use_action_results.ts | 4 +- .../agent_policies/agents_policy_link.tsx | 4 +- .../agent_policies/use_agent_policies.ts | 1 - ...managed_policy_create_import_extension.tsx | 2 +- x-pack/plugins/osquery/public/index.ts | 2 +- .../live_queries/agent_results/index.tsx | 30 ---- .../public/live_queries/form/index.tsx | 149 +++++++++++------- x-pack/plugins/osquery/public/plugin.ts | 10 +- .../osquery/public/queries/edit/tabs.tsx | 78 --------- .../osquery/public/results/results_table.tsx | 74 ++++++--- .../osquery/public/results/translations.ts | 2 +- .../osquery/public/results/use_all_results.ts | 2 +- .../routes/live_queries/details/index.tsx | 92 +---------- .../routes/saved_queries/edit/index.tsx | 2 +- .../public/routes/saved_queries/edit/tabs.tsx | 27 ++-- .../public/saved_queries/form/index.tsx | 8 +- .../public/saved_queries/use_saved_queries.ts | 2 +- .../queries/query_flyout.tsx | 27 +++- .../scheduled_query_groups/queries/schema.tsx | 10 +- .../public/shared_components/index.tsx | 8 + .../shared_components/lazy_osquery_action.tsx | 18 +++ .../osquery_action/index.tsx | 99 ++++++++++++ x-pack/plugins/osquery/public/types.ts | 6 +- .../factory/results/query.all_results.dsl.ts | 7 +- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - yarn.lock | 8 +- 34 files changed, 625 insertions(+), 435 deletions(-) create mode 100644 x-pack/plugins/osquery/public/action_results/action_agents_status.tsx create mode 100644 x-pack/plugins/osquery/public/action_results/action_agents_status_badges.tsx create mode 100644 x-pack/plugins/osquery/public/action_results/action_agents_status_bar.tsx create mode 100644 x-pack/plugins/osquery/public/action_results/services/agent_status.tsx create mode 100644 x-pack/plugins/osquery/public/action_results/types.ts delete mode 100644 x-pack/plugins/osquery/public/live_queries/agent_results/index.tsx delete mode 100644 x-pack/plugins/osquery/public/queries/edit/tabs.tsx create mode 100644 x-pack/plugins/osquery/public/shared_components/index.tsx create mode 100644 x-pack/plugins/osquery/public/shared_components/lazy_osquery_action.tsx create mode 100644 x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx diff --git a/package.json b/package.json index 688c73f44a4efd..8f56d80c584ea6 100644 --- a/package.json +++ b/package.json @@ -351,7 +351,7 @@ "react-moment-proptypes": "^1.7.0", "react-monaco-editor": "^0.41.2", "react-popper-tooltip": "^2.10.1", - "react-query": "^3.13.10", + "react-query": "^3.18.1", "react-redux": "^7.2.0", "react-resizable": "^1.7.5", "react-resize-detector": "^4.2.0", diff --git a/x-pack/plugins/osquery/public/action_results/action_agents_status.tsx b/x-pack/plugins/osquery/public/action_results/action_agents_status.tsx new file mode 100644 index 00000000000000..2de5ab11664aea --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/action_agents_status.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useEffect, useMemo, useState } from 'react'; + +import { Direction } from '../../common/search_strategy'; +import { AgentStatusBar } from './action_agents_status_bar'; +import { ActionAgentsStatusBadges } from './action_agents_status_badges'; +import { useActionResults } from './use_action_results'; + +interface ActionAgentsStatusProps { + actionId: string; + expirationDate?: string; + agentIds?: string[]; +} + +const ActionAgentsStatusComponent: React.FC = ({ + actionId, + expirationDate, + agentIds, +}) => { + const [isLive, setIsLive] = useState(true); + const expired = useMemo(() => (!expirationDate ? false : new Date(expirationDate) < new Date()), [ + expirationDate, + ]); + const { + // @ts-expect-error update types + data: { aggregations }, + } = useActionResults({ + actionId, + activePage: 0, + agentIds, + limit: 0, + direction: Direction.asc, + sortField: '@timestamp', + isLive, + }); + + const agentStatus = useMemo(() => { + const notRespondedCount = !agentIds?.length ? 0 : agentIds.length - aggregations.totalResponded; + + return { + success: aggregations.successful, + pending: notRespondedCount, + failed: aggregations.failed, + }; + }, [agentIds?.length, aggregations.failed, aggregations.successful, aggregations.totalResponded]); + + useEffect( + () => + setIsLive(() => { + if (!agentIds?.length || expired) return false; + + return !!(aggregations.totalResponded !== agentIds?.length); + }), + [agentIds?.length, aggregations.totalResponded, expired] + ); + + return ( + <> + + + + + + + + + + + + + + + + + ); +}; + +export const ActionAgentsStatus = React.memo(ActionAgentsStatusComponent); diff --git a/x-pack/plugins/osquery/public/action_results/action_agents_status_badges.tsx b/x-pack/plugins/osquery/public/action_results/action_agents_status_badges.tsx new file mode 100644 index 00000000000000..95b96ca4546109 --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/action_agents_status_badges.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiHealth, EuiNotificationBadge, EuiFlexItem } from '@elastic/eui'; +import React, { memo } from 'react'; + +import { + AGENT_STATUSES, + getColorForAgentStatus, + getLabelForAgentStatus, +} from './services/agent_status'; +import type { ActionAgentStatus } from './types'; + +export const ActionAgentsStatusBadges = memo<{ + agentStatus: { [k in ActionAgentStatus]: number }; + expired: boolean; +}>(({ agentStatus, expired }) => ( + + {AGENT_STATUSES.map((status) => ( + + + + ))} + +)); + +ActionAgentsStatusBadges.displayName = 'ActionAgentsStatusBadges'; + +const AgentStatusBadge = memo<{ expired: boolean; status: ActionAgentStatus; count: number }>( + ({ expired, status, count }) => ( + <> + + + {getLabelForAgentStatus(status, expired)} + + + {count} + + + + + + ) +); + +AgentStatusBadge.displayName = 'AgentStatusBadge'; diff --git a/x-pack/plugins/osquery/public/action_results/action_agents_status_bar.tsx b/x-pack/plugins/osquery/public/action_results/action_agents_status_bar.tsx new file mode 100644 index 00000000000000..21866566cb7e3a --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/action_agents_status_bar.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import styled from 'styled-components'; +import { EuiColorPaletteDisplay } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import { AGENT_STATUSES, getColorForAgentStatus } from './services/agent_status'; +import type { ActionAgentStatus } from './types'; + +const StyledEuiColorPaletteDisplay = styled(EuiColorPaletteDisplay)` + &.osquery-action-agent-status-bar { + border: none; + border-radius: 0; + &:after { + border: none; + } + } +`; + +export const AgentStatusBar: React.FC<{ + agentStatus: { [k in ActionAgentStatus]: number }; +}> = ({ agentStatus }) => { + const palette = useMemo(() => { + let stop = 0; + return AGENT_STATUSES.reduce((acc, status) => { + stop += agentStatus[status] || 0; + acc.push({ + stop, + color: getColorForAgentStatus(status), + }); + return acc; + }, [] as Array<{ stop: number; color: string }>); + }, [agentStatus]); + return ( + + ); +}; diff --git a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx index 257c89047aab0c..d3b0e38a5e033b 100644 --- a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx @@ -8,20 +8,8 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { i18n } from '@kbn/i18n'; -import { - EuiLink, - EuiFlexGroup, - EuiFlexItem, - EuiCard, - EuiTextColor, - EuiSpacer, - EuiDescriptionList, - EuiInMemoryTable, - EuiCodeBlock, - EuiProgress, -} from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; -import styled from 'styled-components'; +import { EuiLink, EuiInMemoryTable, EuiCodeBlock } from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { PLUGIN_ID } from '../../../fleet/common'; import { pagePathGetters } from '../../../fleet/public'; @@ -30,15 +18,10 @@ import { useAllResults } from '../results/use_all_results'; import { Direction } from '../../common/search_strategy'; import { useKibana } from '../common/lib/kibana'; -const StyledEuiCard = styled(EuiCard)` - position: relative; -`; - interface ActionResultsSummaryProps { actionId: string; - expirationDate: Date; + expirationDate?: string; agentIds?: string[]; - isLive?: boolean; } const renderErrorMessage = (error: string) => ( @@ -51,14 +34,16 @@ const ActionResultsSummaryComponent: React.FC = ({ actionId, expirationDate, agentIds, - isLive, }) => { const getUrlForApp = useKibana().services.application.getUrlForApp; // @ts-expect-error update types const [pageIndex, setPageIndex] = useState(0); // @ts-expect-error update types const [pageSize, setPageSize] = useState(50); - const expired = useMemo(() => expirationDate < new Date(), [expirationDate]); + const expired = useMemo(() => (!expirationDate ? false : new Date(expirationDate) < new Date()), [ + expirationDate, + ]); + const [isLive, setIsLive] = useState(true); const { // @ts-expect-error update types data: { aggregations, edges }, @@ -69,7 +54,7 @@ const ActionResultsSummaryComponent: React.FC = ({ limit: pageSize, direction: Direction.asc, sortField: '@timestamp', - isLive: !expired && isLive, + isLive, }); const { data: logsResults } = useAllResults({ @@ -82,64 +67,15 @@ const ActionResultsSummaryComponent: React.FC = ({ direction: Direction.asc, }, ], - isLive: !expired && isLive, + isLive, }); - const notRespondedCount = useMemo(() => { - if (!agentIds || !aggregations.totalResponded) { - return '-'; - } - - return agentIds.length - aggregations.totalResponded; - }, [aggregations.totalResponded, agentIds]); - - const listItems = useMemo( - () => [ - { - title: i18n.translate( - 'xpack.osquery.liveQueryActionResults.summary.agentsQueriedLabelText', - { - defaultMessage: 'Agents queried', - } - ), - description: agentIds?.length, - }, - { - title: i18n.translate('xpack.osquery.liveQueryActionResults.summary.successfulLabelText', { - defaultMessage: 'Successful', - }), - description: aggregations.successful, - }, - { - title: expired - ? i18n.translate('xpack.osquery.liveQueryActionResults.summary.expiredLabelText', { - defaultMessage: 'Expired', - }) - : i18n.translate('xpack.osquery.liveQueryActionResults.summary.pendingLabelText', { - defaultMessage: 'Not yet responded', - }), - description: notRespondedCount, - }, - { - title: i18n.translate('xpack.osquery.liveQueryActionResults.summary.failedLabelText', { - defaultMessage: 'Failed', - }), - description: ( - - {aggregations.failed} - - ), - }, - ], - [agentIds, aggregations.failed, aggregations.successful, notRespondedCount, expired] - ); - const renderAgentIdColumn = useCallback( (agentId) => ( @@ -236,30 +172,26 @@ const ActionResultsSummaryComponent: React.FC = ({ [] ); - return ( - <> - - - - {!expired && notRespondedCount ? : null} - - - - + useEffect(() => { + setIsLive(() => { + if (!agentIds?.length || expired) return false; - {edges.length ? ( - <> - - - - ) : null} - - ); + const uniqueAgentsRepliedCount = + // @ts-expect-error update types + logsResults?.rawResponse.aggregations?.unique_agents.value ?? 0; + + return !!(uniqueAgentsRepliedCount !== agentIds?.length - aggregations.failed); + }); + }, [ + agentIds?.length, + aggregations.failed, + expired, + logsResults?.rawResponse.aggregations?.unique_agents, + ]); + + return edges.length ? ( + + ) : null; }; export const ActionResultsSummary = React.memo(ActionResultsSummaryComponent); diff --git a/x-pack/plugins/osquery/public/action_results/services/agent_status.tsx b/x-pack/plugins/osquery/public/action_results/services/agent_status.tsx new file mode 100644 index 00000000000000..39a033f49ec908 --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/services/agent_status.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { euiPaletteColorBlindBehindText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import type { ActionAgentStatus } from '../types'; + +const visColors = euiPaletteColorBlindBehindText(); +const colorToHexMap = { + default: '#d3dae6', + primary: visColors[1], + secondary: visColors[0], + accent: visColors[2], + warning: visColors[5], + danger: visColors[9], +}; + +export const AGENT_STATUSES: ActionAgentStatus[] = ['success', 'pending', 'failed']; + +export function getColorForAgentStatus(agentStatus: ActionAgentStatus): string { + switch (agentStatus) { + case 'success': + return colorToHexMap.secondary; + case 'pending': + return colorToHexMap.default; + case 'failed': + return colorToHexMap.danger; + default: + throw new Error(`Unsupported action agent status ${agentStatus}`); + } +} + +export function getLabelForAgentStatus(agentStatus: ActionAgentStatus, expired: boolean): string { + switch (agentStatus) { + case 'success': + return i18n.translate('xpack.osquery.liveQueryActionResults.summary.successfulLabelText', { + defaultMessage: 'Successful', + }); + case 'pending': + return expired + ? i18n.translate('xpack.osquery.liveQueryActionResults.summary.expiredLabelText', { + defaultMessage: 'Expired', + }) + : i18n.translate('xpack.osquery.liveQueryActionResults.summary.pendingLabelText', { + defaultMessage: 'Not yet responded', + }); + case 'failed': + return i18n.translate('xpack.osquery.liveQueryActionResults.summary.failedLabelText', { + defaultMessage: 'Failed', + }); + default: + throw new Error(`Unsupported action agent status ${agentStatus}`); + } +} diff --git a/x-pack/plugins/osquery/public/action_results/types.ts b/x-pack/plugins/osquery/public/action_results/types.ts new file mode 100644 index 00000000000000..ce9415986ba022 --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/types.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type ActionAgentStatus = 'success' | 'pending' | 'failed'; diff --git a/x-pack/plugins/osquery/public/action_results/use_action_results.ts b/x-pack/plugins/osquery/public/action_results/use_action_results.ts index ab69bf86dc326c..29bff0819956a8 100644 --- a/x-pack/plugins/osquery/public/action_results/use_action_results.ts +++ b/x-pack/plugins/osquery/public/action_results/use_action_results.ts @@ -83,7 +83,7 @@ export const useActionResults = ({ const totalResponded = // @ts-expect-error update types - responseData.rawResponse?.aggregations?.aggs.responses_by_action_id?.doc_count; + responseData.rawResponse?.aggregations?.aggs.responses_by_action_id?.doc_count ?? 0; const aggsBuckets = // @ts-expect-error update types responseData.rawResponse?.aggregations?.aggs.responses_by_action_id?.responses.buckets; @@ -120,7 +120,7 @@ export const useActionResults = ({ failed: 0, }, }, - refetchInterval: isLive ? 1000 : false, + refetchInterval: isLive ? 5000 : false, keepPreviousData: true, enabled: !skip && !!agentIds?.length, onSuccess: () => setErrorToast(), diff --git a/x-pack/plugins/osquery/public/agent_policies/agents_policy_link.tsx b/x-pack/plugins/osquery/public/agent_policies/agents_policy_link.tsx index 98402e349c77d5..81953135b53210 100644 --- a/x-pack/plugins/osquery/public/agent_policies/agents_policy_link.tsx +++ b/x-pack/plugins/osquery/public/agent_policies/agents_policy_link.tsx @@ -27,7 +27,7 @@ const AgentsPolicyLinkComponent: React.FC = ({ policyId } const href = useMemo( () => getUrlForApp(PLUGIN_ID, { - path: `#` + pagePathGetters.policy_details({ policyId }), + path: `#` + pagePathGetters.policy_details({ policyId })[1], }), [getUrlForApp, policyId] ); @@ -38,7 +38,7 @@ const AgentsPolicyLinkComponent: React.FC = ({ policyId } event.preventDefault(); return navigateToApp(PLUGIN_ID, { - path: `#` + pagePathGetters.policy_details({ policyId }), + path: `#` + pagePathGetters.policy_details({ policyId })[1], }); } }, diff --git a/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts b/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts index 6f876106671987..c51f2d2f44a5c8 100644 --- a/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts +++ b/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts @@ -30,7 +30,6 @@ export const useAgentPolicies = () => { }), { initialData: { items: [], total: 0, page: 1, perPage: 100 }, - placeholderData: [], keepPreviousData: true, select: (response) => response.items, onSuccess: () => setErrorToast(), diff --git a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx index 28d69a6a7b15a1..63036f5f693f76 100644 --- a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx +++ b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx @@ -57,7 +57,7 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo< return getUrlForApp(PLUGIN_ID, { path: `#` + - pagePathGetters.policy_details({ policyId: policy?.policy_id }) + + pagePathGetters.policy_details({ policyId: policy?.policy_id })[1] + '?openEnrollmentFlyout=true', }); }, [getUrlForApp, policy?.policy_id]); diff --git a/x-pack/plugins/osquery/public/index.ts b/x-pack/plugins/osquery/public/index.ts index f0e956b64ee063..fadd61cce85efd 100644 --- a/x-pack/plugins/osquery/public/index.ts +++ b/x-pack/plugins/osquery/public/index.ts @@ -13,4 +13,4 @@ import { OsqueryPlugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new OsqueryPlugin(initializerContext); } -export { OsqueryPluginSetup, OsqueryPluginStart } from './types'; +export type { OsqueryPluginSetup, OsqueryPluginStart } from './types'; diff --git a/x-pack/plugins/osquery/public/live_queries/agent_results/index.tsx b/x-pack/plugins/osquery/public/live_queries/agent_results/index.tsx deleted file mode 100644 index d1ef18e2e12ea8..00000000000000 --- a/x-pack/plugins/osquery/public/live_queries/agent_results/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; -import React from 'react'; -import { useParams } from 'react-router-dom'; - -import { useActionDetails } from '../../actions/use_action_details'; -import { ResultsTable } from '../../results/results_table'; - -const QueryAgentResultsComponent = () => { - const { actionId, agentId } = useParams<{ actionId: string; agentId: string }>(); - const { data } = useActionDetails({ actionId }); - - return ( - <> - - {data?.actionDetails._source?.data?.query} - - - - - ); -}; - -export const QueryAgentResults = React.memo(QueryAgentResultsComponent); diff --git a/x-pack/plugins/osquery/public/live_queries/form/index.tsx b/x-pack/plugins/osquery/public/live_queries/form/index.tsx index 9e952810e3352c..8654a74fecfb40 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/index.tsx @@ -18,6 +18,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback, useMemo, useState } from 'react'; import { useMutation } from 'react-query'; +import deepMerge from 'deepmerge'; import { UseField, Form, FormData, useForm, useFormData, FIELD_TYPES } from '../../shared_imports'; import { AgentsTableField } from './agents_table_field'; @@ -33,12 +34,19 @@ const FORM_ID = 'liveQueryForm'; export const MAX_QUERY_LENGTH = 2000; +const GhostFormField = () => <>; + interface LiveQueryFormProps { + agentId?: string | undefined; defaultValue?: Partial | undefined; onSuccess?: () => void; } -const LiveQueryFormComponent: React.FC = ({ defaultValue, onSuccess }) => { +const LiveQueryFormComponent: React.FC = ({ + agentId, + defaultValue, + onSuccess, +}) => { const { http } = useKibana().services; const [showSavedQueryFlyout, setShowSavedQueryFlyout] = useState(false); const setErrorToast = useErrorToast(); @@ -71,8 +79,6 @@ const LiveQueryFormComponent: React.FC = ({ defaultValue, on } ); - const expirationDate = useMemo(() => new Date(data?.actions[0].expiration), [data?.actions]); - const formSchema = { query: { type: FIELD_TYPES.TEXT, @@ -100,9 +106,18 @@ const LiveQueryFormComponent: React.FC = ({ defaultValue, on options: { stripEmptyFields: false, }, - defaultValue: defaultValue ?? { - query: '', - }, + defaultValue: deepMerge( + { + agentSelection: { + agents: [], + allAgentsSelected: false, + platformsSelected: [], + policiesSelected: [], + }, + query: '', + }, + defaultValue ?? {} + ), }); const { submit } = form; @@ -147,6 +162,59 @@ const LiveQueryFormComponent: React.FC = ({ defaultValue, on const flyoutFormDefaultValue = useMemo(() => ({ query }), [query]); + const queryFieldStepContent = useMemo( + () => ( + <> + + + + {!agentId && ( + + + + + + )} + + + + + + + + ), + [ + agentId, + agentSelected, + handleShowSaveQueryFlout, + queryComponentProps, + queryValueProvided, + resultsStatus, + submit, + ] + ); + + const resultsStepContent = useMemo( + () => + actionId ? ( + + ) : null, + [actionId, agentIds, data?.actions] + ); + const formSteps: EuiContainedStepProps[] = useMemo( () => [ { @@ -160,73 +228,34 @@ const LiveQueryFormComponent: React.FC = ({ defaultValue, on title: i18n.translate('xpack.osquery.liveQueryForm.steps.queryStepHeading', { defaultMessage: 'Enter query', }), - children: ( - <> - - - - - - - - - - - - - - - - ), + children: queryFieldStepContent, status: queryStatus, }, { title: i18n.translate('xpack.osquery.liveQueryForm.steps.resultsStepHeading', { defaultMessage: 'Check results', }), - children: actionId ? ( - - ) : null, + children: resultsStepContent, status: resultsStatus, }, ], - [ - actionId, - agentIds, - agentSelected, - handleShowSaveQueryFlout, - queryComponentProps, - queryStatus, - queryValueProvided, - expirationDate, - resultsStatus, - submit, - ] + [agentSelected, queryFieldStepContent, queryStatus, resultsStepContent, resultsStatus] + ); + + const singleAgentForm = useMemo( + () => ( + + + {queryFieldStepContent} + {resultsStepContent} + + ), + [queryFieldStepContent, resultsStepContent] ); return ( <> -
- - +
{agentId ? singleAgentForm : } {showSavedQueryFlyout ? ( , @@ -160,7 +161,14 @@ export class OsqueryPlugin implements Plugin = ({ - actionId, - agentIds, - expirationDate, - endDate, - isLive, - startDate, -}) => { - const tabs = useMemo( - () => [ - { - id: 'status', - name: 'Status', - content: ( - <> - - - - ), - }, - { - id: 'results', - name: 'Results', - content: ( - <> - - - - ), - }, - ], - [actionId, agentIds, endDate, isLive, startDate, expirationDate] - ); - - return ( - - ); -}; - -export const ResultTabs = React.memo(ResultTabsComponent); diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx index 6ff60d30d23bf7..d82737ab51e7cc 100644 --- a/x-pack/plugins/osquery/public/results/results_table.tsx +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -14,6 +14,8 @@ import { EuiDataGridColumn, EuiLink, EuiLoadingContent, + EuiProgress, + EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react'; @@ -37,17 +39,16 @@ interface ResultsTableComponentProps { selectedAgent?: string; agentIds?: string[]; endDate?: string; - isLive?: boolean; startDate?: string; } const ResultsTableComponent: React.FC = ({ actionId, agentIds, - isLive, startDate, endDate, }) => { + const [isLive, setIsLive] = useState(true); const { // @ts-expect-error update types data: { aggregations }, @@ -60,13 +61,13 @@ const ResultsTableComponent: React.FC = ({ sortField: '@timestamp', isLive, }); - + const expired = useMemo(() => (!endDate ? false : new Date(endDate) < new Date()), [endDate]); const { getUrlForApp } = useKibana().services.application; const getFleetAppUrl = useCallback( (agentId) => getUrlForApp('fleet', { - path: `#` + pagePathGetters.agent_details({ agentId }), + path: `#` + pagePathGetters.agent_details({ agentId })[1], }), [getUrlForApp] ); @@ -216,29 +217,56 @@ const ResultsTableComponent: React.FC = ({ [actionId, endDate, startDate] ); - if (!aggregations.totalResponded) { - return ; - } + useEffect( + () => + setIsLive(() => { + if (!agentIds?.length || expired) return false; + + const uniqueAgentsRepliedCount = + // @ts-expect-error-type + allResultsData?.rawResponse.aggregations?.unique_agents.value ?? 0; + + return !!(uniqueAgentsRepliedCount !== agentIds?.length - aggregations.failed); + }), + [ + agentIds?.length, + aggregations.failed, + // @ts-expect-error-type + allResultsData?.rawResponse.aggregations?.unique_agents.value, + expired, + ] + ); - if (aggregations.totalResponded && isFetched && !allResultsData?.edges.length) { - return ; + if (!isFetched) { + return ; } return ( - // @ts-expect-error update types - - - + <> + {isLive && } + + {isFetched && !allResultsData?.edges.length ? ( + <> + + + + ) : ( + // @ts-expect-error update types + + + + )} + ); }; diff --git a/x-pack/plugins/osquery/public/results/translations.ts b/x-pack/plugins/osquery/public/results/translations.ts index 8e77e78ec76e29..e4f71d818f01d3 100644 --- a/x-pack/plugins/osquery/public/results/translations.ts +++ b/x-pack/plugins/osquery/public/results/translations.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; export const generateEmptyDataMessage = (agentsResponded: number): string => { return i18n.translate('xpack.osquery.results.multipleAgentsResponded', { defaultMessage: - '{agentsResponded, plural, one {# agent has} other {# agents have}} responded, but no osquery data has been reported.', + '{agentsResponded, plural, one {# agent has} other {# agents have}} responded, no osquery data has been reported.', values: { agentsResponded }, }); }; diff --git a/x-pack/plugins/osquery/public/results/use_all_results.ts b/x-pack/plugins/osquery/public/results/use_all_results.ts index 11218984102786..a13fceedfa07a3 100644 --- a/x-pack/plugins/osquery/public/results/use_all_results.ts +++ b/x-pack/plugins/osquery/public/results/use_all_results.ts @@ -78,7 +78,7 @@ export const useAllResults = ({ }; }, { - refetchInterval: isLive ? 1000 : false, + refetchInterval: isLive ? 5000 : false, enabled: !skip, onSuccess: () => setErrorToast(), onError: (error: Error) => diff --git a/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx b/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx index e4f1bb447a15a2..02f5c8b6fb2a58 100644 --- a/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx +++ b/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx @@ -6,54 +6,24 @@ */ import { get } from 'lodash'; -import { - EuiButtonEmpty, - EuiTextColor, - EuiFlexGroup, - EuiFlexItem, - EuiCodeBlock, - EuiSpacer, - EuiDescriptionList, - EuiDescriptionListTitle, - EuiDescriptionListDescription, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiCodeBlock, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; import { useParams } from 'react-router-dom'; -import styled from 'styled-components'; -import { Direction } from '../../../../common/search_strategy'; import { useRouterNavigate } from '../../../common/lib/kibana'; import { WithHeaderLayout } from '../../../components/layouts'; -import { useActionResults } from '../../../action_results/use_action_results'; import { useActionDetails } from '../../../actions/use_action_details'; import { ResultTabs } from '../../saved_queries/edit/tabs'; import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs'; import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge'; -const Divider = styled.div` - width: 0; - height: 100%; - border-left: ${({ theme }) => theme.eui.euiBorderThin}; -`; - const LiveQueryDetailsPageComponent = () => { const { actionId } = useParams<{ actionId: string }>(); useBreadcrumbs('live_query_details', { liveQueryId: actionId }); const liveQueryListProps = useRouterNavigate('live_queries'); const { data } = useActionDetails({ actionId }); - const expirationDate = useMemo(() => new Date(data?.actionDetails._source.expiration), [ - data?.actionDetails, - ]); - const expired = useMemo(() => expirationDate < new Date(), [expirationDate]); - const { data: actionResultsData } = useActionResults({ - actionId, - activePage: 0, - limit: 0, - direction: Direction.asc, - sortField: '@timestamp', - }); const LeftColumn = useMemo( () => ( @@ -82,72 +52,14 @@ const LiveQueryDetailsPageComponent = () => { [liveQueryListProps] ); - const failed = useMemo(() => { - let result = actionResultsData?.aggregations.failed; - if (expired) { - result = '-'; - if (data?.actionDetails?.fields?.agents && actionResultsData?.aggregations) { - result = - data.actionDetails.fields.agents.length - actionResultsData.aggregations.successful; - } - } - return result; - }, [expired, actionResultsData?.aggregations, data?.actionDetails?.fields?.agents]); - - const RightColumn = useMemo( - () => ( - - - <> - - - - - - {/* eslint-disable-next-line react-perf/jsx-no-new-object-as-prop */} - - - - - - {data?.actionDetails?.fields?.agents?.length ?? '0'} - - - - - - - - {/* eslint-disable-next-line react-perf/jsx-no-new-object-as-prop */} - - - - - - {failed} - - - - - ), - [data?.actionDetails?.fields?.agents?.length, failed] - ); - return ( - + {data?.actionDetails._source?.data?.query} { const updateSavedQueryMutation = useUpdateSavedQuery({ savedQueryId }); const deleteSavedQueryMutation = useDeleteSavedQuery({ savedQueryId }); - useBreadcrumbs('saved_query_edit', { savedQueryId: savedQueryDetails?.attributes?.id ?? '' }); + useBreadcrumbs('saved_query_edit', { savedQueryName: savedQueryDetails?.attributes?.id ?? '' }); const handleCloseDeleteConfirmationModal = useCallback(() => { setIsDeleteModalVisible(false); diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/edit/tabs.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/edit/tabs.tsx index 1946cd6dd3450f..1f56daaa3bdb52 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/edit/tabs.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/edit/tabs.tsx @@ -10,12 +10,11 @@ import React, { useMemo } from 'react'; import { ResultsTable } from '../../../results/results_table'; import { ActionResultsSummary } from '../../../action_results/action_results_summary'; +import { ActionAgentsStatus } from '../../../action_results/action_agents_status'; interface ResultTabsProps { actionId: string; agentIds?: string[]; - expirationDate: Date; - isLive?: boolean; startDate?: string; endDate?: string; } @@ -24,8 +23,6 @@ const ResultTabsComponent: React.FC = ({ actionId, agentIds, endDate, - expirationDate, - isLive, startDate, }) => { const tabs = useMemo( @@ -39,7 +36,6 @@ const ResultTabsComponent: React.FC = ({ @@ -55,23 +51,26 @@ const ResultTabsComponent: React.FC = ({ ), }, ], - [actionId, agentIds, endDate, expirationDate, isLive, startDate] + [actionId, agentIds, endDate, startDate] ); return ( - + <> + + + + ); }; diff --git a/x-pack/plugins/osquery/public/saved_queries/form/index.tsx b/x-pack/plugins/osquery/public/saved_queries/form/index.tsx index 174227eb5e6e5f..9bbf847c4d2a0d 100644 --- a/x-pack/plugins/osquery/public/saved_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/saved_queries/form/index.tsx @@ -7,6 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; import React from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ALL_OSQUERY_VERSIONS_OPTIONS } from '../../scheduled_query_groups/queries/constants'; @@ -57,7 +58,12 @@ const SavedQueryFormComponent = () => ( euiFieldProps={{ noSuggestions: false, singleSelection: { asPlainText: true }, - placeholder: ALL_OSQUERY_VERSIONS_OPTIONS[0].label, + placeholder: i18n.translate( + 'xpack.osquery.scheduledQueryGroup.queriesTable.osqueryVersionAllLabel', + { + defaultMessage: 'ALL', + } + ), options: ALL_OSQUERY_VERSIONS_OPTIONS, onCreateOption: undefined, }} diff --git a/x-pack/plugins/osquery/public/saved_queries/use_saved_queries.ts b/x-pack/plugins/osquery/public/saved_queries/use_saved_queries.ts index 324d4aace16474..bb5a73d9d50fa9 100644 --- a/x-pack/plugins/osquery/public/saved_queries/use_saved_queries.ts +++ b/x-pack/plugins/osquery/public/saved_queries/use_saved_queries.ts @@ -40,7 +40,7 @@ export const useSavedQueries = ({ { keepPreviousData: true, // Refetch the data every 10 seconds - refetchInterval: isLive ? 10000 : false, + refetchInterval: isLive ? 5000 : false, } ); }; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx index b37c315849f603..32547bc5dd2d08 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx @@ -18,9 +18,10 @@ import { EuiFlexItem, EuiButtonEmpty, EuiButton, - EuiDescribedFormGroup, + EuiText, } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { satisfies } from 'semver'; @@ -128,10 +129,7 @@ const QueryFlyoutComponent: React.FC = ({ - Set heading level based on context} - description={'Will be wrapped in a small, subdued EuiText block.'} - > + = ({ + + + + + } // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop euiFieldProps={{ isDisabled: !isFieldSupported, noSuggestions: false, singleSelection: { asPlainText: true }, - placeholder: ALL_OSQUERY_VERSIONS_OPTIONS[0].label, + placeholder: i18n.translate( + 'xpack.osquery.scheduledQueryGroup.queriesTable.osqueryVersionAllLabel', + { + defaultMessage: 'ALL', + } + ), options: ALL_OSQUERY_VERSIONS_OPTIONS, onCreateOption: undefined, }} @@ -160,7 +173,7 @@ const QueryFlyoutComponent: React.FC = ({ euiFieldProps={{ disabled: !isFieldSupported }} /> - + {!isFieldSupported ? ( diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx index d8dbaad2f17e82..0b23ce924f9301 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -65,14 +65,6 @@ export const formSchema = { defaultMessage="Minimum Osquery version" /> - - - - - ) as unknown) as string, validations: [], diff --git a/x-pack/plugins/osquery/public/shared_components/index.tsx b/x-pack/plugins/osquery/public/shared_components/index.tsx new file mode 100644 index 00000000000000..0e0ab4e5c045ba --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_components/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getLazyOsqueryAction } from './lazy_osquery_action'; diff --git a/x-pack/plugins/osquery/public/shared_components/lazy_osquery_action.tsx b/x-pack/plugins/osquery/public/shared_components/lazy_osquery_action.tsx new file mode 100644 index 00000000000000..9cc55a65ce2bca --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_components/lazy_osquery_action.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { lazy, Suspense } from 'react'; + +// @ts-expect-error update types +export const getLazyOsqueryAction = (services) => (props) => { + const OsqueryAction = lazy(() => import('./osquery_action')); + return ( + + + + ); +}; diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx new file mode 100644 index 00000000000000..cf8a85cea244cc --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiErrorBoundary, EuiLoadingContent } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { QueryClientProvider } from 'react-query'; +import { KibanaContextProvider, useKibana } from '../../common/lib/kibana'; + +import { LiveQueryForm } from '../../live_queries/form'; +import { queryClient } from '../../query_client'; + +interface OsqueryActionProps { + hostId?: string | undefined; +} + +const OsqueryActionComponent: React.FC = ({ hostId }) => { + const [agentId, setAgentId] = useState(); + const { indexPatterns, search } = useKibana().services.data; + + useEffect(() => { + if (hostId) { + const findAgent = async () => { + const searchSource = await search.searchSource.create(); + const indexPattern = await indexPatterns.find('.fleet-agents'); + + searchSource.setField('index', indexPattern[0]); + searchSource.setField('filter', [ + { + meta: { + alias: null, + disabled: false, + negate: false, + key: 'local_metadata.host.id', + value: hostId, + }, + query: { + match_phrase: { + 'local_metadata.host.id': hostId, + }, + }, + }, + { + meta: { + alias: null, + disabled: false, + negate: false, + key: 'active', + value: 'true', + }, + query: { + match_phrase: { + active: 'true', + }, + }, + }, + ]); + + const response = await searchSource.fetch$().toPromise(); + + if (response.rawResponse.hits.hits.length && response.rawResponse.hits.hits[0]._id) { + setAgentId(response.rawResponse.hits.hits[0]._id); + } + }; + + findAgent(); + } + }); + + if (!agentId) { + return ; + } + + return ( + // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop + + ); +}; + +export const OsqueryAction = React.memo(OsqueryActionComponent); + +// @ts-expect-error update types +const OsqueryActionWrapperComponent = ({ services, ...props }) => ( + + + + + + + +); + +const OsqueryActionWrapper = React.memo(OsqueryActionWrapperComponent); + +// eslint-disable-next-line import/no-default-export +export { OsqueryActionWrapper as default }; diff --git a/x-pack/plugins/osquery/public/types.ts b/x-pack/plugins/osquery/public/types.ts index 9a466dfc619b60..fd21b39d25504e 100644 --- a/x-pack/plugins/osquery/public/types.ts +++ b/x-pack/plugins/osquery/public/types.ts @@ -16,11 +16,13 @@ import { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '../../triggers_actions_ui/public'; +import { getLazyOsqueryAction } from './shared_components'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface OsqueryPluginSetup {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface OsqueryPluginStart {} +export interface OsqueryPluginStart { + OsqueryAction?: ReturnType; +} export interface AppPluginStartDependencies { navigation: NavigationPublicPluginStart; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts index b560fd3c364e91..406ff26991f0e2 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts @@ -47,12 +47,17 @@ export const buildResultsQuery = ({ size: 10000, }, }, + unique_agents: { + cardinality: { + field: 'elastic_agent.id', + }, + }, }, query: { bool: { filter } }, from: activePage * querySize, size: querySize, track_total_hits: true, - fields: agentId ? ['osquery.*'] : ['agent.*', 'osquery.*'], + fields: ['elastic_agent.*', 'agent.*', 'osquery.*'], sort: sort?.map((sortConfig) => ({ [sortConfig.field]: { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3809d55ff90bea..11770d2d2f3866 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17301,7 +17301,6 @@ "xpack.osquery.fleetIntegration.scheduleQueryGroupsButtonText": "クエリグループをスケジュール", "xpack.osquery.liveQueriesHistory.newLiveQueryButtonLabel": "新しいライブクエリ", "xpack.osquery.liveQueriesHistory.pageTitle": "ライブクエリ履歴", - "xpack.osquery.liveQueryActionResults.summary.agentsQueriedLabelText": "エージェントがクエリされました", "xpack.osquery.liveQueryActionResults.summary.failedLabelText": "失敗", "xpack.osquery.liveQueryActionResults.summary.pendingLabelText": "未応答", "xpack.osquery.liveQueryActionResults.summary.successfulLabelText": "成功", @@ -17316,8 +17315,6 @@ "xpack.osquery.liveQueryActions.table.createdAtColumnTitle": "作成日時:", "xpack.osquery.liveQueryActions.table.queryColumnTitle": "クエリ", "xpack.osquery.liveQueryActions.table.viewDetailsColumnTitle": "詳細を表示", - "xpack.osquery.liveQueryDetails.kpis.agentsFailedCountLabelText": "エージェントが失敗しました", - "xpack.osquery.liveQueryDetails.kpis.agentsQueriedLabelText": "エージェントがクエリされました", "xpack.osquery.liveQueryDetails.pageTitle": "ライブクエリ詳細", "xpack.osquery.liveQueryDetails.viewLiveQueriesHistoryTitle": "ライブクエリ履歴を表示", "xpack.osquery.liveQueryForm.form.submitButtonLabel": "送信", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6540d9f49399aa..9704070feb8ab0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17539,7 +17539,6 @@ "xpack.osquery.fleetIntegration.scheduleQueryGroupsButtonText": "计划查询组", "xpack.osquery.liveQueriesHistory.newLiveQueryButtonLabel": "新建实时查询", "xpack.osquery.liveQueriesHistory.pageTitle": "实时查询历史记录", - "xpack.osquery.liveQueryActionResults.summary.agentsQueriedLabelText": "查询的代理", "xpack.osquery.liveQueryActionResults.summary.failedLabelText": "失败", "xpack.osquery.liveQueryActionResults.summary.pendingLabelText": "尚未响应", "xpack.osquery.liveQueryActionResults.summary.successfulLabelText": "成功", @@ -17554,8 +17553,6 @@ "xpack.osquery.liveQueryActions.table.createdAtColumnTitle": "创建于", "xpack.osquery.liveQueryActions.table.queryColumnTitle": "查询", "xpack.osquery.liveQueryActions.table.viewDetailsColumnTitle": "查看详情", - "xpack.osquery.liveQueryDetails.kpis.agentsFailedCountLabelText": "失败的代理", - "xpack.osquery.liveQueryDetails.kpis.agentsQueriedLabelText": "查询的代理", "xpack.osquery.liveQueryDetails.pageTitle": "实时查询详情", "xpack.osquery.liveQueryDetails.viewLiveQueriesHistoryTitle": "查看实时查询历史记录", "xpack.osquery.liveQueryForm.form.submitButtonLabel": "提交", diff --git a/yarn.lock b/yarn.lock index 51df4a1e72b90d..c64f4e1c4b2810 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23088,10 +23088,10 @@ react-popper@^2.2.4: react-fast-compare "^3.0.1" warning "^4.0.2" -react-query@^3.13.10: - version "3.13.10" - resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.13.10.tgz#b6a05e22a5debb6e2df79ada588179771cbd7df8" - integrity sha512-wFvKhEDnOVL5bFL+9KPgNsiOOei1Ad+l6l1awCBuoX7xMG+SXXKDOF2uuZFsJe0w6gdthdWN+00021yepTR31g== +react-query@^3.18.1: + version "3.18.1" + resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.18.1.tgz#893b5475a7b4add099e007105317446f7a2cd310" + integrity sha512-17lv3pQxU9n+cB5acUv0/cxNTjo9q8G+RsedC6Ax4V9D8xEM7Q5xf9xAbCPdEhDrrnzPjTls9fQEABKRSi7OJA== dependencies: "@babel/runtime" "^7.5.5" broadcast-channel "^3.4.1" From 94a0b23486fc5e68b0c14a3de8d943f111ad674d Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 8 Jul 2021 17:11:42 -0700 Subject: [PATCH 28/77] [reporting] remove outdated todo comment (#104959) Co-authored-by: spalger --- src/dev/build/tasks/install_chromium.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dev/build/tasks/install_chromium.js b/src/dev/build/tasks/install_chromium.js index 37abcbad4466eb..95e0df8984f9da 100644 --- a/src/dev/build/tasks/install_chromium.js +++ b/src/dev/build/tasks/install_chromium.js @@ -19,7 +19,6 @@ export const InstallChromium = { log.info(`Installing Chromium for ${platform.getName()}-${platform.getArchitecture()}`); const { binaryPath$ } = installBrowser( - // TODO: https://github.com/elastic/kibana/issues/72496 log, build.resolvePathForPlatform(platform, 'x-pack/plugins/reporting/chromium'), platform.getName(), From c6916eacc2ca391ee4e832e4f173f453fa70fb75 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Thu, 8 Jul 2021 18:12:04 -0600 Subject: [PATCH 29/77] [Security Solution][Exceptions] Gets rid of rule exception comma delimiter for "is one of" operator (#104960) --- .../components/autocomplete/field_value_match_any.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx index 82347f62124425..e5a5e76f8cc5d6 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx @@ -164,7 +164,6 @@ export const AutocompleteFieldMatchAnyComponent: React.FC From fcb0de47f1ea94254b2d8e6ca83f1cc88b8f760a Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 8 Jul 2021 19:55:18 -0500 Subject: [PATCH 30/77] [paths] Add default lookup for kibana.yml at /etc/kibana (#103934) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-utils/src/path/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kbn-utils/src/path/index.ts b/packages/kbn-utils/src/path/index.ts index 2d6d0e9e919eb2..9835179a61e9d5 100644 --- a/packages/kbn-utils/src/path/index.ts +++ b/packages/kbn-utils/src/path/index.ts @@ -18,6 +18,7 @@ const CONFIG_PATHS = [ process.env.KIBANA_PATH_CONF && join(process.env.KIBANA_PATH_CONF, 'kibana.yml'), process.env.CONFIG_PATH, // deprecated join(REPO_ROOT, 'config/kibana.yml'), + '/etc/kibana/kibana.yml', ].filter(isString); const CONFIG_DIRECTORIES = [ From 1d2811e6036a383975ed71ddf67c92c44c8da2b3 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Thu, 8 Jul 2021 22:45:01 -0400 Subject: [PATCH 31/77] [Security Solution] add advanced policy option for Linux Malware quarantine (#104984) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../pages/policy/models/advanced_policy_schema.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts index 62d51c3630db75..db998b871cd93e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts @@ -691,4 +691,15 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ } ), }, + { + key: 'linux.advanced.malware.quarantine', + first_supported_version: '7.14', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.malware.quarantine', + { + defaultMessage: + 'Whether quarantine should be enabled when malware prevention is enabled. Default: true.', + } + ), + }, ]; From d4e46c52a274671fb8cef7521cbeae749bcb59a9 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 9 Jul 2021 08:13:36 +0100 Subject: [PATCH 32/77] [ML] Fixing annotations table loading hang (#104825) --- .../annotations/annotations_table/annotations_table.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 7d461c4ec8572a..ed603357206ad9 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -128,6 +128,12 @@ class AnnotationsTableUI extends Component { jobId: undefined, }); }); + } else { + this.setState({ + annotations: [], + isLoading: false, + jobId: undefined, + }); } } From 8dd08529ae764d9691869625720c29d4a2feafa3 Mon Sep 17 00:00:00 2001 From: Dmitry Shevchenko Date: Fri, 9 Jul 2021 10:24:36 +0200 Subject: [PATCH 33/77] Fix stale validation messages on the action edit form (#104868) --- .../detection_rules/custom_query_rule.spec.ts | 18 ++++++++++++++ .../cypress/objects/connector.ts | 24 +++++++++++++++++++ .../cypress/screens/create_new_rule.ts | 24 +++++++++++++++++++ .../cypress/tasks/create_new_rule.ts | 16 +++++++++++++ .../rules/rule_actions_field/index.tsx | 8 ++++++- 5 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/cypress/objects/connector.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts index 5f9175476795ce..218b1f7745d947 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts @@ -48,6 +48,12 @@ import { SEVERITY_DROPDOWN, TAGS_CLEAR_BUTTON, TAGS_FIELD, + EMAIL_ACTION_BTN, + CREATE_ACTION_CONNECTOR_BTN, + SAVE_ACTION_CONNECTOR_BTN, + FROM_VALIDATION_ERROR, + EMAIL_ACTION_TO_INPUT, + EMAIL_ACTION_SUBJECT_INPUT, } from '../../screens/create_new_rule'; import { ADDITIONAL_LOOK_BACK_DETAILS, @@ -99,6 +105,7 @@ import { fillAboutRule, fillAboutRuleAndContinue, fillDefineCustomRuleWithImportedQueryAndContinue, + fillEmailConnectorForm, fillScheduleRuleAndContinue, goToAboutStepTab, goToActionsStepTab, @@ -360,6 +367,17 @@ describe('Custom detection rules deletion and edition', () => { cy.get(ACTIONS_THROTTLE_INPUT).invoke('val').should('eql', 'no_actions'); + cy.get(ACTIONS_THROTTLE_INPUT).select('Weekly'); + cy.get(EMAIL_ACTION_BTN).click(); + cy.get(CREATE_ACTION_CONNECTOR_BTN).click(); + fillEmailConnectorForm(); + cy.get(SAVE_ACTION_CONNECTOR_BTN).click(); + + cy.get(EMAIL_ACTION_TO_INPUT).type('test@example.com'); + cy.get(EMAIL_ACTION_SUBJECT_INPUT).type('Subject'); + + cy.get(FROM_VALIDATION_ERROR).should('not.exist'); + goToAboutStepTab(); cy.get(TAGS_CLEAR_BUTTON).click({ force: true }); fillAboutRule(editedRule); diff --git a/x-pack/plugins/security_solution/cypress/objects/connector.ts b/x-pack/plugins/security_solution/cypress/objects/connector.ts new file mode 100644 index 00000000000000..2a0f1cc43eff0e --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/objects/connector.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface EmailConnector { + name: string; + from: string; + host: string; + port: string; + user: string; + password: string; +} + +export const emailConnector: EmailConnector = { + name: 'Test connector', + from: 'test@example.com', + host: 'example.com', + port: '80', + user: 'username', + password: 'password', +}; diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index a580068b636e49..551857ca3bfcae 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -16,6 +16,30 @@ export const ACTIONS_EDIT_TAB = '[data-test-subj="edit-rule-actions-tab"]'; export const ACTIONS_THROTTLE_INPUT = '[data-test-subj="stepRuleActions"] [data-test-subj="select"]'; +export const EMAIL_ACTION_BTN = '[data-test-subj=".email-ActionTypeSelectOption"]'; + +export const CREATE_ACTION_CONNECTOR_BTN = '[data-test-subj="createActionConnectorButton-0"]'; + +export const SAVE_ACTION_CONNECTOR_BTN = '[data-test-subj="saveActionButtonModal"]'; + +export const EMAIL_ACTION_TO_INPUT = '[data-test-subj="toEmailAddressInput"]'; + +export const EMAIL_ACTION_SUBJECT_INPUT = '[data-test-subj="subjectInput"]'; + +export const FROM_VALIDATION_ERROR = '.euiFormErrorText'; + +export const CONNECTOR_NAME_INPUT = '[data-test-subj="nameInput"]'; + +export const EMAIL_CONNECTOR_FROM_INPUT = '[data-test-subj="emailFromInput"]'; + +export const EMAIL_CONNECTOR_HOST_INPUT = '[data-test-subj="emailHostInput"]'; + +export const EMAIL_CONNECTOR_PORT_INPUT = '[data-test-subj="emailPortInput"]'; + +export const EMAIL_CONNECTOR_USER_INPUT = '[data-test-subj="emailUserInput"]'; + +export const EMAIL_CONNECTOR_PASSWORD_INPUT = '[data-test-subj="emailPasswordInput"]'; + export const ADD_FALSE_POSITIVE_BTN = '[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] .euiButtonEmpty__text'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 9c15b1f03932df..9b74110f0ef77d 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { emailConnector, EmailConnector } from '../objects/connector'; import { CustomRule, MachineLearningRule, @@ -85,6 +86,12 @@ import { THRESHOLD_FIELD_SELECTION, THRESHOLD_INPUT_AREA, THRESHOLD_TYPE, + CONNECTOR_NAME_INPUT, + EMAIL_CONNECTOR_FROM_INPUT, + EMAIL_CONNECTOR_HOST_INPUT, + EMAIL_CONNECTOR_PORT_INPUT, + EMAIL_CONNECTOR_USER_INPUT, + EMAIL_CONNECTOR_PASSWORD_INPUT, } from '../screens/create_new_rule'; import { TOAST_ERROR } from '../screens/shared'; import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; @@ -390,6 +397,15 @@ export const fillIndexAndIndicatorIndexPattern = ( getIndicatorIndicatorIndex().type(`${indicatorIndex}{enter}`); }; +export const fillEmailConnectorForm = (connector: EmailConnector = emailConnector) => { + cy.get(CONNECTOR_NAME_INPUT).type(connector.name); + cy.get(EMAIL_CONNECTOR_FROM_INPUT).type(connector.from); + cy.get(EMAIL_CONNECTOR_HOST_INPUT).type(connector.host); + cy.get(EMAIL_CONNECTOR_PORT_INPUT).type(connector.port); + cy.get(EMAIL_CONNECTOR_USER_INPUT).type(connector.user); + cy.get(EMAIL_CONNECTOR_PASSWORD_INPUT).type(connector.password); +}; + /** Returns the indicator index drop down field. Pass in row number, default is 1 */ export const getIndicatorIndexComboField = (row = 1) => cy.get(THREAT_COMBO_BOX_INPUT).eq(row * 2 - 2); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx index 9ecf3333279eb4..2206960f6bcd3a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx @@ -122,7 +122,13 @@ export const RuleActionsField: React.FC = ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any (key: string, value: any, index: number) => { const updatedActions = [...actions]; - updatedActions[index].params[key] = value; + updatedActions[index] = { + ...updatedActions[index], + params: { + ...updatedActions[index].params, + [key]: value, + }, + }; field.setValue(updatedActions); }, // eslint-disable-next-line react-hooks/exhaustive-deps From 7ddab693ae149390196bf0e3a6c24b11f8360288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ester=20Mart=C3=AD=20Vilaseca?= Date: Fri, 9 Jul 2021 10:35:24 +0200 Subject: [PATCH 34/77] [Monitoring] Add rules modal to listing page (#104328) * Add rules modal to listing page * Fix tests * minor fixes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/alerts/enable_alerts_modal.tsx | 14 ++++---- .../public/services/enable_alerts_modal.js | 6 ++-- .../public/views/cluster/listing/index.js | 32 ++++++++++++------- .../public/views/cluster/overview/index.js | 2 +- .../apps/monitoring/cluster/list.js | 4 +++ .../test/functional/apps/monitoring/index.js | 2 +- .../services/monitoring/cluster_list.js | 5 +++ 7 files changed, 41 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/monitoring/public/alerts/enable_alerts_modal.tsx b/x-pack/plugins/monitoring/public/alerts/enable_alerts_modal.tsx index 914446c42aaa79..fadf4c5872507a 100644 --- a/x-pack/plugins/monitoring/public/alerts/enable_alerts_modal.tsx +++ b/x-pack/plugins/monitoring/public/alerts/enable_alerts_modal.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useState, useContext } from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiButton, @@ -22,14 +22,16 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { AlertsContext } from './context'; import { Legacy } from '../legacy_shims'; -export const EnableAlertsModal: React.FC<{}> = () => { +interface Props { + alerts: {}; +} + +export const EnableAlertsModal: React.FC = ({ alerts }: Props) => { const [isModalVisible, setIsModalVisible] = useState(false); const $injector = Legacy.shims.getAngularInjector(); const alertsEnableModalProvider: any = $injector.get('enableAlertsModal'); - const alertsContext = useContext(AlertsContext); const closeModal = () => { setIsModalVisible(false); @@ -58,10 +60,10 @@ export const EnableAlertsModal: React.FC<{}> = () => { }; useEffect(() => { - if (alertsEnableModalProvider.shouldShowAlertsModal(alertsContext)) { + if (alertsEnableModalProvider.shouldShowAlertsModal(alerts)) { setIsModalVisible(true); } - }, [alertsEnableModalProvider, alertsContext]); + }, [alertsEnableModalProvider, alerts]); const confirmButtonClick = () => { if (radioIdSelected === 'create-alerts') { diff --git a/x-pack/plugins/monitoring/public/services/enable_alerts_modal.js b/x-pack/plugins/monitoring/public/services/enable_alerts_modal.js index 0232e302517afa..438c5ab83f5e30 100644 --- a/x-pack/plugins/monitoring/public/services/enable_alerts_modal.js +++ b/x-pack/plugins/monitoring/public/services/enable_alerts_modal.js @@ -12,12 +12,10 @@ export function enableAlertsModalProvider($http, $window, $injector) { const modalHasBeenShown = $window.sessionStorage.getItem('ALERTS_MODAL_HAS_BEEN_SHOWN'); const decisionMade = $window.localStorage.getItem('ALERTS_MODAL_DECISION_MADE'); - if (Object.keys(alerts.allAlerts).length > 0) { + if (Object.keys(alerts).length > 0) { $window.localStorage.setItem('ALERTS_MODAL_DECISION_MADE', true); return false; - } - - if (!modalHasBeenShown && !decisionMade) { + } else if (!modalHasBeenShown && !decisionMade) { return true; } diff --git a/x-pack/plugins/monitoring/public/views/cluster/listing/index.js b/x-pack/plugins/monitoring/public/views/cluster/listing/index.js index 9f9eec3848604d..8b365292aeb139 100644 --- a/x-pack/plugins/monitoring/public/views/cluster/listing/index.js +++ b/x-pack/plugins/monitoring/public/views/cluster/listing/index.js @@ -13,6 +13,7 @@ import { MonitoringViewBaseEuiTableController } from '../../'; import template from './index.html'; import { Listing } from '../../../components/cluster/listing'; import { CODE_PATH_ALL } from '../../../../common/constants'; +import { EnableAlertsModal } from '../../../alerts/enable_alerts_modal.tsx'; const CODE_PATHS = [CODE_PATH_ALL]; @@ -21,6 +22,10 @@ const getPageData = ($injector) => { return monitoringClusters(undefined, undefined, CODE_PATHS); }; +const getAlerts = (clusters) => { + return clusters.reduce((alerts, cluster) => ({ ...alerts, ...cluster.alerts.list }), {}); +}; + uiRoutes .when('/home', { template, @@ -71,18 +76,21 @@ uiRoutes () => this.data, (data) => { this.renderReact( -
+ <> + + + ); } ); diff --git a/x-pack/plugins/monitoring/public/views/cluster/overview/index.js b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js index bf34650bdb700d..20e694ad8548f0 100644 --- a/x-pack/plugins/monitoring/public/views/cluster/overview/index.js +++ b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js @@ -83,7 +83,7 @@ uiRoutes.when('/overview', { setupMode={setupMode} showLicenseExpiration={showLicenseExpiration} /> - + {bottomBarComponent} )} diff --git a/x-pack/test/functional/apps/monitoring/cluster/list.js b/x-pack/test/functional/apps/monitoring/cluster/list.js index f88e30f717141e..09361f88f56529 100644 --- a/x-pack/test/functional/apps/monitoring/cluster/list.js +++ b/x-pack/test/functional/apps/monitoring/cluster/list.js @@ -26,6 +26,8 @@ export default function ({ getService, getPageObjects }) { to: 'Aug 16, 2017 @ 00:00:00.000', }); + await clusterList.closeAlertsModal(); + await clusterList.assertDefaults(); }); @@ -83,6 +85,8 @@ export default function ({ getService, getPageObjects }) { to: 'Sep 7, 2017 @ 20:18:55.733', }); + await clusterList.closeAlertsModal(); + await clusterList.assertDefaults(); }); diff --git a/x-pack/test/functional/apps/monitoring/index.js b/x-pack/test/functional/apps/monitoring/index.js index 24ace88f334f0d..213007c7b71df4 100644 --- a/x-pack/test/functional/apps/monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/index.js @@ -46,7 +46,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./beats/listing')); loadTestFile(require.resolve('./beats/beat_detail')); - loadTestFile(require.resolve('./time_filter')); + // loadTestFile(require.resolve('./time_filter')); loadTestFile(require.resolve('./enable_monitoring')); loadTestFile(require.resolve('./setup/metricbeat_migration')); diff --git a/x-pack/test/functional/services/monitoring/cluster_list.js b/x-pack/test/functional/services/monitoring/cluster_list.js index aea82fbb6b793f..f63e7b6cd125ec 100644 --- a/x-pack/test/functional/services/monitoring/cluster_list.js +++ b/x-pack/test/functional/services/monitoring/cluster_list.js @@ -15,6 +15,7 @@ export function MonitoringClusterListProvider({ getService, getPageObjects }) { const SUBJ_SEARCH_BAR = `${SUBJ_TABLE_CONTAINER} > monitoringTableToolBar`; const SUBJ_CLUSTER_ROW_PREFIX = `${SUBJ_TABLE_CONTAINER} > clusterRow_`; + const ALERTS_MODAL_BUTTON = 'alerts-modal-button'; return new (class ClusterList { async assertDefaults() { @@ -41,6 +42,10 @@ export function MonitoringClusterListProvider({ getService, getPageObjects }) { return PageObjects.monitoring.tableClearFilter(SUBJ_SEARCH_BAR); } + closeAlertsModal() { + return testSubjects.click(ALERTS_MODAL_BUTTON); + } + getClusterLink(clusterUuid) { return testSubjects.find(`${SUBJ_CLUSTER_ROW_PREFIX}${clusterUuid} > clusterLink`); } From ffc37406bd8985402f434be86d7bd181d7c26a3c Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 9 Jul 2021 12:13:00 +0200 Subject: [PATCH 35/77] [APM] Get service name from context for alert flyout (#104103) --- .../alerting/alerting_flyout/index.tsx | 13 ++- .../error_count_alert_trigger/index.tsx | 57 +++++++------ .../alerting/service_alert_trigger/index.tsx | 4 - .../index.tsx | 85 +++++++++---------- .../index.tsx | 55 ++++++------ .../index.tsx | 70 +++++++-------- .../shared/apm_header_action_menu/index.tsx | 4 +- .../apm_service/apm_service_context.tsx | 12 ++- .../use_service_agent_name_fetcher.ts | 4 +- .../use_service_alerts_fetcher.tsx | 10 ++- .../use_service_transaction_types_fetcher.tsx | 4 +- .../apm/public/hooks/use_service_name.tsx | 16 ++++ 12 files changed, 184 insertions(+), 150 deletions(-) create mode 100644 x-pack/plugins/apm/public/hooks/use_service_name.tsx diff --git a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx index b87298c5fe8a09..eef3271d5932d6 100644 --- a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx @@ -6,7 +6,6 @@ */ import React, { useCallback, useMemo } from 'react'; -import { useParams } from 'react-router-dom'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { AlertType, @@ -14,6 +13,8 @@ import { } from '../../../../common/alert_types'; import { getInitialAlertValues } from '../get_initial_alert_values'; import { ApmPluginStartDeps } from '../../../plugin'; +import { useServiceName } from '../../../hooks/use_service_name'; +import { ApmServiceContextProvider } from '../../../context/apm_service/apm_service_context'; interface Props { addFlyoutVisible: boolean; setAddFlyoutVisibility: React.Dispatch>; @@ -22,7 +23,7 @@ interface Props { export function AlertingFlyout(props: Props) { const { addFlyoutVisible, setAddFlyoutVisibility, alertType } = props; - const { serviceName } = useParams<{ serviceName?: string }>(); + const serviceName = useServiceName(); const { services } = useKibana(); const initialValues = getInitialAlertValues(alertType, serviceName); @@ -43,5 +44,11 @@ export function AlertingFlyout(props: Props) { /* eslint-disable-next-line react-hooks/exhaustive-deps */ [alertType, onCloseAddFlyout, services.triggersActionsUi] ); - return <>{addFlyoutVisible && addAlertFlyout}; + return ( + <> + {addFlyoutVisible && ( + {addAlertFlyout} + )} + + ); } diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx index fdfed6eb0d6857..811353067ab60e 100644 --- a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { useParams } from 'react-router-dom'; +import { defaults } from 'lodash'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { asInteger } from '../../../../common/utils/formatters'; @@ -18,6 +18,7 @@ import { ChartPreview } from '../chart_preview'; import { EnvironmentField, IsAboveField, ServiceField } from '../fields'; import { getAbsoluteTimeRange } from '../helper'; import { ServiceAlertTrigger } from '../service_alert_trigger'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; export interface AlertParams { windowSize: number; @@ -35,49 +36,55 @@ interface Props { export function ErrorCountAlertTrigger(props: Props) { const { setAlertParams, setAlertProperty, alertParams } = props; - const { serviceName } = useParams<{ serviceName?: string }>(); + + const { serviceName: serviceNameFromContext } = useApmServiceContext(); + const { urlParams } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, environment: environmentFromUrl } = urlParams; const { environmentOptions } = useEnvironmentsFetcher({ - serviceName, + serviceName: serviceNameFromContext, start, end, }); - const { threshold, windowSize, windowUnit, environment } = alertParams; + const params = defaults( + { + ...alertParams, + }, + { + threshold: 25, + windowSize: 1, + windowUnit: 'm', + environment: environmentFromUrl || ENVIRONMENT_ALL.value, + serviceName: serviceNameFromContext, + } + ); const { data } = useFetcher( (callApmApi) => { - if (windowSize && windowUnit) { + if (params.windowSize && params.windowUnit) { return callApmApi({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count', params: { query: { - ...getAbsoluteTimeRange(windowSize, windowUnit), - environment, - serviceName, + ...getAbsoluteTimeRange(params.windowSize, params.windowUnit), + environment: params.environment, + serviceName: params.serviceName, }, }, }); } }, - [windowSize, windowUnit, environment, serviceName] + [ + params.windowSize, + params.windowUnit, + params.environment, + params.serviceName, + ] ); - const defaults = { - threshold: 25, - windowSize: 1, - windowUnit: 'm', - environment: urlParams.environment || ENVIRONMENT_ALL.value, - }; - - const params = { - ...defaults, - ...alertParams, - }; - const fields = [ - , + , ); return ( void; @@ -18,13 +17,10 @@ interface Props { } export function ServiceAlertTrigger(props: Props) { - const { serviceName } = useParams<{ serviceName?: string }>(); - const { fields, setAlertParams, defaults, chartPreview } = props; const params: Record = { ...defaults, - serviceName, }; useEffect(() => { diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx index b4c78b54f329b4..8f2713685127e9 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx @@ -7,9 +7,8 @@ import { EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { map } from 'lodash'; +import { map, defaults } from 'lodash'; import React from 'react'; -import { useParams } from 'react-router-dom'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { getDurationFormatter } from '../../../../common/utils/formatters'; @@ -72,46 +71,60 @@ interface Props { export function TransactionDurationAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const { transactionTypes, transactionType } = useApmServiceContext(); - const { serviceName } = useParams<{ serviceName?: string }>(); - const { start, end } = urlParams; + + const { start, end, environment: environmentFromUrl } = urlParams; + + const { + transactionTypes, + transactionType: transactionTypeFromContext, + serviceName: serviceNameFromContext, + } = useApmServiceContext(); + + const params = defaults( + { + ...alertParams, + }, + { + aggregationType: 'avg', + environment: environmentFromUrl || ENVIRONMENT_ALL.value, + threshold: 1500, + windowSize: 5, + windowUnit: 'm', + transactionType: transactionTypeFromContext, + serviceName: serviceNameFromContext, + } + ); + const { environmentOptions } = useEnvironmentsFetcher({ - serviceName, + serviceName: params.serviceName, start, end, }); - const { - aggregationType, - environment, - threshold, - windowSize, - windowUnit, - } = alertParams; const { data } = useFetcher( (callApmApi) => { - if (windowSize && windowUnit) { + if (params.windowSize && params.windowUnit) { return callApmApi({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration', params: { query: { - ...getAbsoluteTimeRange(windowSize, windowUnit), - aggregationType, - environment, - serviceName, - transactionType: alertParams.transactionType, + ...getAbsoluteTimeRange(params.windowSize, params.windowUnit), + aggregationType: params.aggregationType, + environment: params.environment, + serviceName: params.serviceName, + transactionType: params.transactionType, }, }, }); } }, [ - aggregationType, - environment, - serviceName, - alertParams.transactionType, - windowSize, - windowUnit, + params.aggregationType, + params.environment, + params.serviceName, + params.transactionType, + params.windowSize, + params.windowUnit, ] ); @@ -122,7 +135,7 @@ export function TransactionDurationAlertTrigger(props: Props) { const yTickFormat = getResponseTimeTickFormatter(formatter); // The threshold from the form is in ms. Convert to µs. - const thresholdMs = threshold * 1000; + const thresholdMs = params.threshold * 1000; const chartPreview = ( ); - if (!transactionTypes.length || !serviceName) { + if (!transactionTypes.length || !params.serviceName) { return null; } - const defaults = { - threshold: 1500, - aggregationType: 'avg', - windowSize: 5, - windowUnit: 'm', - transactionType, - environment: urlParams.environment || ENVIRONMENT_ALL.value, - }; - - const params = { - ...defaults, - ...alertParams, - }; - const fields = [ - , + , ({ text: key, value: key }))} @@ -206,7 +205,7 @@ export function TransactionDurationAlertTrigger(props: Props) { return ( void; setAlertProperty: (key: string, value: any) => void; } @@ -47,35 +47,36 @@ interface Props { export function TransactionDurationAnomalyAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const { transactionTypes } = useApmServiceContext(); - const { serviceName } = useParams<{ serviceName?: string }>(); - const { start, end, transactionType } = urlParams; + const { + serviceName: serviceNameFromContext, + transactionType: transactionTypeFromContext, + transactionTypes, + } = useApmServiceContext(); + + const { start, end, environment: environmentFromUrl } = urlParams; + + const params = defaults( + { + ...alertParams, + }, + { + windowSize: 15, + windowUnit: 'm', + transactionType: transactionTypeFromContext, + environment: environmentFromUrl || ENVIRONMENT_ALL.value, + anomalySeverityType: ANOMALY_SEVERITY.CRITICAL, + serviceName: serviceNameFromContext, + } + ); + const { environmentOptions } = useEnvironmentsFetcher({ - serviceName, + serviceName: params.serviceName, start, end, }); - if (serviceName && !transactionTypes.length) { - return null; - } - - const defaults: Params = { - windowSize: 15, - windowUnit: 'm', - transactionType: transactionType || transactionTypes[0], - serviceName, - environment: urlParams.environment || ENVIRONMENT_ALL.value, - anomalySeverityType: ANOMALY_SEVERITY.CRITICAL, - }; - - const params = { - ...defaults, - ...alertParams, - }; - const fields = [ - , + , ({ text: key, value: key }))} @@ -107,7 +108,7 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { return ( diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx index c6f9c4efd98b61..4eb0b0e7975712 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { useParams } from 'react-router-dom'; +import { defaults } from 'lodash'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { asPercent } from '../../../../common/utils/formatters'; @@ -42,63 +42,65 @@ interface Props { export function TransactionErrorRateAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const { transactionTypes } = useApmServiceContext(); - const { serviceName } = useParams<{ serviceName?: string }>(); - const { start, end, transactionType } = urlParams; + const { + transactionType: transactionTypeFromContext, + transactionTypes, + serviceName: serviceNameFromContext, + } = useApmServiceContext(); + + const { start, end, environment: environmentFromUrl } = urlParams; + + const params = defaults, AlertParams>( + { + threshold: 30, + windowSize: 5, + windowUnit: 'm', + transactionType: transactionTypeFromContext, + environment: environmentFromUrl || ENVIRONMENT_ALL.value, + serviceName: serviceNameFromContext, + }, + alertParams + ); + const { environmentOptions } = useEnvironmentsFetcher({ - serviceName, + serviceName: serviceNameFromContext, start, end, }); - const { threshold, windowSize, windowUnit, environment } = alertParams; - - const thresholdAsPercent = (threshold ?? 0) / 100; + const thresholdAsPercent = (params.threshold ?? 0) / 100; const { data } = useFetcher( (callApmApi) => { - if (windowSize && windowUnit) { + if (params.windowSize && params.windowUnit) { return callApmApi({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate', params: { query: { - ...getAbsoluteTimeRange(windowSize, windowUnit), - environment, - serviceName, - transactionType: alertParams.transactionType, + ...getAbsoluteTimeRange(params.windowSize, params.windowUnit), + environment: params.environment, + serviceName: params.serviceName, + transactionType: params.transactionType, }, }, }); } }, [ - alertParams.transactionType, - environment, - serviceName, - windowSize, - windowUnit, + params.transactionType, + params.environment, + params.serviceName, + params.windowSize, + params.windowUnit, ] ); - if (serviceName && !transactionTypes.length) { + if (params.serviceName && !transactionTypes.length) { return null; } - const defaultParams = { - threshold: 30, - windowSize: 5, - windowUnit: 'm', - transactionType: transactionType || transactionTypes[0], - environment: urlParams.environment || ENVIRONMENT_ALL.value, - }; - - const params = { - ...defaultParams, - ...alertParams, - }; - const fields = [ - , + , ({ text: key, value: key }))} @@ -141,7 +143,7 @@ export function TransactionErrorRateAlertTrigger(props: Props) { return ( (); + const serviceName = useServiceName(); const { search } = window.location; const { application, http } = core; const { basePath } = http; diff --git a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx index 54914580aefbd6..cb826763425c2b 100644 --- a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx @@ -18,6 +18,7 @@ import { useServiceAgentNameFetcher } from './use_service_agent_name_fetcher'; import { IUrlParams } from '../url_params_context/types'; import { APIReturnType } from '../../services/rest/createCallApmApi'; import { useServiceAlertsFetcher } from './use_service_alerts_fetcher'; +import { useServiceName } from '../../hooks/use_service_name'; export type APMServiceAlert = ValuesType< APIReturnType<'GET /api/apm/services/{serviceName}/alerts'>['alerts'] @@ -28,6 +29,7 @@ export const APMServiceContext = createContext<{ transactionType?: string; transactionTypes: string[]; alerts: APMServiceAlert[]; + serviceName?: string; }>({ transactionTypes: [], alerts: [] }); export function ApmServiceContextProvider({ @@ -36,9 +38,12 @@ export function ApmServiceContextProvider({ children: ReactNode; }) { const { urlParams } = useUrlParams(); - const { agentName } = useServiceAgentNameFetcher(); - const transactionTypes = useServiceTransactionTypesFetcher(); + const serviceName = useServiceName(); + + const { agentName } = useServiceAgentNameFetcher(serviceName); + + const transactionTypes = useServiceTransactionTypesFetcher(serviceName); const transactionType = getTransactionType({ urlParams, @@ -46,7 +51,7 @@ export function ApmServiceContextProvider({ agentName, }); - const { alerts } = useServiceAlertsFetcher(transactionType); + const { alerts } = useServiceAlertsFetcher({ serviceName, transactionType }); return ( diff --git a/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts b/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts index ceb6767898f06c..82198eb73b3cb9 100644 --- a/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts +++ b/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts @@ -5,12 +5,10 @@ * 2.0. */ -import { useParams } from 'react-router-dom'; import { useFetcher } from '../../hooks/use_fetcher'; import { useUrlParams } from '../url_params_context/use_url_params'; -export function useServiceAgentNameFetcher() { - const { serviceName } = useParams<{ serviceName?: string }>(); +export function useServiceAgentNameFetcher(serviceName?: string) { const { urlParams } = useUrlParams(); const { start, end } = urlParams; const { data, error, status } = useFetcher( diff --git a/x-pack/plugins/apm/public/context/apm_service/use_service_alerts_fetcher.tsx b/x-pack/plugins/apm/public/context/apm_service/use_service_alerts_fetcher.tsx index b07e6562a21542..54c95319afea2e 100644 --- a/x-pack/plugins/apm/public/context/apm_service/use_service_alerts_fetcher.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/use_service_alerts_fetcher.tsx @@ -5,13 +5,18 @@ * 2.0. */ -import { useParams } from 'react-router-dom'; import { useApmPluginContext } from '../apm_plugin/use_apm_plugin_context'; import { useUrlParams } from '../url_params_context/use_url_params'; import { useFetcher } from '../../hooks/use_fetcher'; import type { APMServiceAlert } from './apm_service_context'; -export function useServiceAlertsFetcher(transactionType?: string) { +export function useServiceAlertsFetcher({ + serviceName, + transactionType, +}: { + serviceName?: string; + transactionType?: string; +}) { const { plugins: { observability }, } = useApmPluginContext(); @@ -19,7 +24,6 @@ export function useServiceAlertsFetcher(transactionType?: string) { const { urlParams: { start, end, environment }, } = useUrlParams(); - const { serviceName } = useParams<{ serviceName?: string }>(); const experimentalAlertsEnabled = observability.isAlertingExperienceEnabled(); diff --git a/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx b/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx index ba70295ae70ca6..b22c233b0c24b6 100644 --- a/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx @@ -5,14 +5,12 @@ * 2.0. */ -import { useParams } from 'react-router-dom'; import { useFetcher } from '../../hooks/use_fetcher'; import { useUrlParams } from '../url_params_context/use_url_params'; const INITIAL_DATA = { transactionTypes: [] }; -export function useServiceTransactionTypesFetcher() { - const { serviceName } = useParams<{ serviceName?: string }>(); +export function useServiceTransactionTypesFetcher(serviceName?: string) { const { urlParams } = useUrlParams(); const { start, end } = urlParams; const { data = INITIAL_DATA } = useFetcher( diff --git a/x-pack/plugins/apm/public/hooks/use_service_name.tsx b/x-pack/plugins/apm/public/hooks/use_service_name.tsx new file mode 100644 index 00000000000000..c003bf5223a32b --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_service_name.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useRouteMatch } from 'react-router-dom'; + +export function useServiceName(): string | undefined { + const match = useRouteMatch<{ serviceName?: string }>( + '/services/:serviceName' + ); + + return match ? match.params.serviceName : undefined; +} From b1f2f1395ddb7c0046d7f591f7345aa05c3e0cc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Fri, 9 Jul 2021 12:28:41 +0200 Subject: [PATCH 36/77] [Security Solution][Endpoint] Enrich events with ES data before rendering event filters modal (#104703) * Enrich events with ES data before rendering event filters modal * Add unmounted component control check * Fix error when closing and opening modal/flyout twice --- .../pages/event_filters/test_utils/index.ts | 101 ++++++++++++ .../view/components/flyout/index.tsx | 9 ++ .../view/components/modal/index.test.tsx | 153 +++++++++++------- .../view/components/modal/index.tsx | 78 +++++++-- 4 files changed, 275 insertions(+), 66 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts index c45d0f88927be5..fb2251072f03da 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts @@ -106,6 +106,107 @@ export type EventFiltersListQueryHttpMockProviders = ResponseProvidersInterface< eventFiltersCreateList: () => ExceptionListItemSchema; }>; +export const esResponseData = () => ({ + rawResponse: { + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: 1, + max_score: 1, + hits: [ + { + _index: '.ds-logs-endpoint.events.process-default-2021.07.06-000001', + _id: 'ZihXfHoBP7UhLrksX9-B', + _score: 1, + _source: { + agent: { + id: '9b5fad11-6cd9-401b-afc1-1c2b0c8a2603', + type: 'endpoint', + version: '7.12.2', + }, + process: { + args: '"C:\\lsass.exe" \\d6e', + Ext: { + ancestry: ['wm6pfs8yo3', 'd0zpkp91jx'], + }, + parent: { + pid: 2356, + entity_id: 'wm6pfs8yo3', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + name: 'lsass.exe', + pid: 2522, + entity_id: 'hmmlst1ewe', + executable: 'C:\\lsass.exe', + hash: { + md5: 'de8c03a1-099f-4d9b-9a5e-1961c18af19f', + }, + }, + network: { + forwarded_ip: '10.105.19.209', + direction: 'inbound', + }, + '@timestamp': 1625694621727, + ecs: { + version: '1.4.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.events.process', + }, + host: { + hostname: 'Host-15ofk0qkwk', + os: { + Ext: { + variant: 'Windows Pro', + }, + name: 'Linux', + family: 'Debian OS', + version: '10.0', + platform: 'Windows', + full: 'Windows 10', + }, + ip: ['10.133.4.77', '10.135.101.75', '10.137.102.119'], + name: 'Host-15ofk0qkwk', + id: 'bae7a849-1ce9-421a-a879-5fee5dcd1fb9', + mac: ['ad-65-2d-17-aa-95', '63-4-33-c5-c6-90'], + architecture: 'uwp8xmxk1f', + }, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 36, + ingested: '2021-07-06T15:02:18.746828Z', + kind: 'event', + id: '02057ac0-0ae5-442c-9082-c5a7489dde09', + category: 'network', + type: 'start', + }, + user: { + domain: '22bk8yptgw', + name: 'dlkfiz43rh', + }, + }, + }, + ], + }, + }, + isPartial: false, + isRunning: false, + total: 1, + loaded: 1, + isRestored: false, +}); + /** * Mock `core.http` methods used by Event Filters List page */ diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx index 9f81d255205248..48523ce45c3f95 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx @@ -70,6 +70,15 @@ export const EventFiltersFlyout: React.FC = memo( payload: { entry: getInitialExceptionFromEvent() }, }); } + + return () => { + dispatch({ + type: 'eventFiltersFormStateChanged', + payload: { + type: 'UninitialisedResourceState', + }, + }); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.test.tsx index 178b774e916358..c77188694f507f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.test.tsx @@ -6,18 +6,24 @@ */ import React from 'react'; import { EventFiltersModal } from '.'; -import { RenderResult, act, render } from '@testing-library/react'; +import { RenderResult, act } from '@testing-library/react'; import { fireEvent } from '@testing-library/dom'; -import { Provider } from 'react-redux'; -import { ThemeProvider } from 'styled-components'; -import { createGlobalNoMiddlewareStore, ecsEventMock } from '../../../test_utils'; -import { getMockTheme } from '../../../../../../common/lib/kibana/kibana_react.mock'; +import { ecsEventMock, esResponseData } from '../../../test_utils'; +import { + AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../../../common/mock/endpoint'; +import { MiddlewareActionSpyHelper } from '../../../../../../common/store/test_utils'; + import { MODAL_TITLE, MODAL_SUBTITLE, ACTIONS_CONFIRM, ACTIONS_CANCEL } from './translations'; import type { CreateExceptionListItemSchema, ExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { useKibana } from '../../../../../../common/lib/kibana'; +import { EventFiltersListPageState } from '../../../types'; +jest.mock('../../../../../../common/lib/kibana'); jest.mock('../form'); jest.mock('../../hooks', () => { const originalModule = jest.requireActual('../../hooks'); @@ -29,67 +35,88 @@ jest.mock('../../hooks', () => { }; }); -const mockTheme = getMockTheme({ - eui: { - paddingSizes: { m: '2' }, - euiBreakpoints: { l: '2' }, - }, -}); - describe('Event filter modal', () => { let component: RenderResult; - let store: ReturnType; let onCancelMock: jest.Mock; - - const renderForm = () => { - const Wrapper: React.FC = ({ children }) => ( - - {children} - - ); - - return render(, { - wrapper: Wrapper, - }); - }; + let mockedContext: AppContextTestRender; + let waitForAction: MiddlewareActionSpyHelper['waitForAction']; + let render: () => ReturnType; + let getState: () => EventFiltersListPageState; beforeEach(() => { - store = createGlobalNoMiddlewareStore(); + mockedContext = createAppRootMockRenderer(); + waitForAction = mockedContext.middlewareSpy.waitForAction; onCancelMock = jest.fn(); + getState = () => mockedContext.store.getState().management.eventFilters; + render = () => + mockedContext.render(); + (useKibana as jest.Mock).mockReturnValue({ + services: { + http: {}, + data: { + search: { + search: jest.fn().mockImplementation(() => ({ toPromise: () => esResponseData() })), + }, + }, + notifications: {}, + }, + }); }); - it('should renders correctly', () => { - component = renderForm(); + it('should renders correctly', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + expect(component.getAllByText(MODAL_TITLE)).not.toBeNull(); expect(component.getByText(MODAL_SUBTITLE)).not.toBeNull(); expect(component.getAllByText(ACTIONS_CONFIRM)).not.toBeNull(); expect(component.getByText(ACTIONS_CANCEL)).not.toBeNull(); }); - it('should dispatch action to init form store on mount', () => { - component = renderForm(); - expect(store.getState()!.management!.eventFilters!.form!.entry).not.toBeNull(); + it('should dispatch action to init form store on mount', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + + expect(getState().form!.entry).not.toBeUndefined(); + }); + + it('should set OS with the enriched data', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + + expect(getState().form!.entry?.os_types).toContain('linux'); }); - it('should confirm form when button is disabled', () => { - component = renderForm(); + it('should confirm form when button is disabled', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + const confirmButton = component.getByTestId('add-exception-confirm-button'); act(() => { fireEvent.click(confirmButton); }); - expect(store.getState()!.management!.eventFilters!.form!.submissionResourceState.type).toBe( - 'UninitialisedResourceState' - ); + expect(getState().form!.submissionResourceState.type).toBe('UninitialisedResourceState'); }); - it('should confirm form when button is enabled', () => { - component = renderForm(); - store.dispatch({ + it('should confirm form when button is enabled', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + + mockedContext.store.dispatch({ type: 'eventFiltersChangeForm', payload: { entry: { - ...(store.getState()!.management!.eventFilters!.form! - .entry as CreateExceptionListItemSchema), + ...(getState().form!.entry as CreateExceptionListItemSchema), name: 'test', }, hasNameError: false, @@ -99,22 +126,24 @@ describe('Event filter modal', () => { act(() => { fireEvent.click(confirmButton); }); - expect(store.getState()!.management!.eventFilters!.form!.submissionResourceState.type).toBe( - 'UninitialisedResourceState' - ); - expect(confirmButton.hasAttribute('disabled')).toBeFalsy(); + expect(getState().form!.submissionResourceState.type).toBe('LoadingResourceState'); + expect(confirmButton.hasAttribute('disabled')).toBeTruthy(); }); - it('should close when exception has been submitted correctly', () => { - component = renderForm(); + it('should close when exception has been submitted correctly', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + expect(onCancelMock).toHaveBeenCalledTimes(0); act(() => { - store.dispatch({ + mockedContext.store.dispatch({ type: 'eventFiltersFormStateChanged', payload: { type: 'LoadedResourceState', - data: store.getState()!.management!.eventFilters!.form!.entry as ExceptionListItemSchema, + data: getState().form!.entry as ExceptionListItemSchema, }, }); }); @@ -122,8 +151,12 @@ describe('Event filter modal', () => { expect(onCancelMock).toHaveBeenCalledTimes(1); }); - it('should close when click on cancel button', () => { - component = renderForm(); + it('should close when click on cancel button', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + const cancelButton = component.getByText(ACTIONS_CANCEL); expect(onCancelMock).toHaveBeenCalledTimes(0); @@ -134,8 +167,12 @@ describe('Event filter modal', () => { expect(onCancelMock).toHaveBeenCalledTimes(1); }); - it('should close when close modal', () => { - component = renderForm(); + it('should close when close modal', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + const modalCloseButton = component.getByLabelText('Closes this modal window'); expect(onCancelMock).toHaveBeenCalledTimes(0); @@ -146,10 +183,14 @@ describe('Event filter modal', () => { expect(onCancelMock).toHaveBeenCalledTimes(1); }); - it('should prevent close when is loading action', () => { - component = renderForm(); + it('should prevent close when is loading action', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + act(() => { - store.dispatch({ + mockedContext.store.dispatch({ type: 'eventFiltersFormStateChanged', payload: { type: 'LoadingResourceState', diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.tsx index 50102d09248b12..dabf68ffed3945 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useMemo, useEffect, useCallback } from 'react'; +import React, { memo, useMemo, useEffect, useCallback, useState, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; import styled, { css } from 'styled-components'; @@ -20,6 +20,7 @@ import { import { AppAction } from '../../../../../../common/store/actions'; import { Ecs } from '../../../../../../../common/ecs'; import { EventFiltersForm } from '../form'; +import { useKibana } from '../../../../../../common/lib/kibana'; import { useEventFiltersSelector, useEventFiltersNotification } from '../../hooks'; import { getFormHasError, @@ -61,10 +62,76 @@ const ModalBodySection = styled.section` export const EventFiltersModal: React.FC = memo(({ data, onCancel }) => { useEventFiltersNotification(); + const [enrichedData, setEnrichedData] = useState(); + const { + data: { search }, + } = useKibana().services; const dispatch = useDispatch>(); const formHasError = useEventFiltersSelector(getFormHasError); const creationInProgress = useEventFiltersSelector(isCreationInProgress); const creationSuccessful = useEventFiltersSelector(isCreationSuccessful); + const isMounted = useRef(false); + + // Enrich the event with missing ECS data from ES source + useEffect(() => { + isMounted.current = true; + + const enrichEvent = async () => { + if (!data._index) return; + const searchResponse = await search + .search({ + params: { + index: data._index, + body: { + query: { + match: { + _id: data._id, + }, + }, + }, + }, + }) + .toPromise(); + + if (!isMounted.current) return; + + setEnrichedData({ + ...data, + host: { + ...data.host, + os: { + ...(data?.host?.os || {}), + name: [searchResponse.rawResponse.hits.hits[0]._source.host.os.name], + }, + }, + }); + }; + + enrichEvent(); + + return () => { + dispatch({ + type: 'eventFiltersFormStateChanged', + payload: { + type: 'UninitialisedResourceState', + }, + }); + isMounted.current = false; + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Initialize the store with the enriched event to allow render the form + useEffect(() => { + if (enrichedData) { + dispatch({ + type: 'eventFiltersInitForm', + payload: { entry: getInitialExceptionFromEvent(enrichedData) }, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [enrichedData]); useEffect(() => { if (creationSuccessful) { @@ -78,15 +145,6 @@ export const EventFiltersModal: React.FC = memo(({ data, } }, [creationSuccessful, onCancel, dispatch]); - // Initialize the store with the event passed as prop to allow render the form. It acts as componentDidMount - useEffect(() => { - dispatch({ - type: 'eventFiltersInitForm', - payload: { entry: getInitialExceptionFromEvent(data) }, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const handleOnCancel = useCallback(() => { if (creationInProgress) return; onCancel(); From 01005289ae6c45e3cd49a61ced61349657df9cd2 Mon Sep 17 00:00:00 2001 From: Dmitry Shevchenko Date: Fri, 9 Jul 2021 12:43:40 +0200 Subject: [PATCH 37/77] Fix incorrect tags after rule duplication (#104948) --- .../server/lib/detection_engine/rules/add_tags.test.ts | 9 +++++++++ .../server/lib/detection_engine/rules/add_tags.ts | 4 +++- .../lib/detection_engine/rules/duplicate_rule.test.ts | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.test.ts index a871c7157d5e83..93fddc06b80685 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.test.ts @@ -33,8 +33,17 @@ describe('add_tags', () => { const tags2 = addTags(tags1, 'rule-1', false); expect(tags2).toEqual([ 'tag-1', + `${INTERNAL_RULE_ID_KEY}:rule-1`, `${INTERNAL_IMMUTABLE_KEY}:false`, + ]); + }); + + test('it should overwrite existing immutable tag if it exists', () => { + const tags1 = addTags(['tag-1', `${INTERNAL_IMMUTABLE_KEY}:true`], 'rule-1', false); + expect(tags1).toEqual([ + 'tag-1', `${INTERNAL_RULE_ID_KEY}:rule-1`, + `${INTERNAL_IMMUTABLE_KEY}:false`, ]); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.ts index 6ff4a54ad8e544..d66f961b385988 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.ts @@ -10,7 +10,9 @@ import { INTERNAL_RULE_ID_KEY, INTERNAL_IMMUTABLE_KEY } from '../../../../common export const addTags = (tags: string[], ruleId: string, immutable: boolean): string[] => { return Array.from( new Set([ - ...tags.filter((tag) => !tag.startsWith(INTERNAL_RULE_ID_KEY)), + ...tags.filter( + (tag) => !(tag.startsWith(INTERNAL_RULE_ID_KEY) || tag.startsWith(INTERNAL_IMMUTABLE_KEY)) + ), `${INTERNAL_RULE_ID_KEY}:${ruleId}`, `${INTERNAL_IMMUTABLE_KEY}:${immutable}`, ]) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts index 3046999a632c62..92b4dcff61b353 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts @@ -123,8 +123,8 @@ describe('duplicateRule', () => { }, "tags": Array [ "test", - "__internal_immutable:false", "__internal_rule_id:newId", + "__internal_immutable:false", ], "throttle": null, } From 3e118c4e23d716d71bbbccd63beeab7da0c7a577 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 9 Jul 2021 15:04:37 +0200 Subject: [PATCH 38/77] [Reporting] First accessibility test (#104410) * makeAllReportingPoliciesUnmanaged -> makeAllReportingIndicesUnmanaged * expose the reporting services on the functional services object shared with a11y * added data-test-subjs for a11y test * added reporting a11y test * updated jest test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../report_listing.test.tsx.snap | 9 +++ .../public/management/report_listing.tsx | 3 +- x-pack/test/accessibility/apps/reporting.ts | 76 +++++++++++++++++++ x-pack/test/accessibility/config.ts | 1 + x-pack/test/functional/services/index.ts | 2 + .../ilm_migration_apis.ts | 4 +- .../services/scenarios.ts | 6 +- 7 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 x-pack/test/accessibility/apps/reporting.ts diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap index 9ce249aa32a1dc..744a3b2d405c3d 100644 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap @@ -425,6 +425,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` >
@@ -1434,6 +1435,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` >
@@ -2457,6 +2459,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` >
@@ -3527,6 +3530,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` >
@@ -4630,6 +4634,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` >
@@ -5700,6 +5705,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` >
@@ -6770,6 +6776,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` >
@@ -7840,6 +7847,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` >
@@ -8910,6 +8918,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` >
diff --git a/x-pack/plugins/reporting/public/management/report_listing.tsx b/x-pack/plugins/reporting/public/management/report_listing.tsx index 749e42de526d32..dd41314b4883fa 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.tsx @@ -151,6 +151,7 @@ class ReportListingUi extends Component { return ( <> @@ -375,7 +376,7 @@ class ReportListingUi extends Component { }), render: (objectTitle: string, record: Job) => { return ( -
+
{objectTitle}
{record.object_type} diff --git a/x-pack/test/accessibility/apps/reporting.ts b/x-pack/test/accessibility/apps/reporting.ts new file mode 100644 index 00000000000000..bccb650fa08cad --- /dev/null +++ b/x-pack/test/accessibility/apps/reporting.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +import { JOB_PARAMS_RISON_CSV_DEPRECATED } from '../../reporting_api_integration/services/fixtures'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const { common } = getPageObjects(['common']); + const retry = getService('retry'); + const a11y = getService('a11y'); + const testSubjects = getService('testSubjects'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const reporting = getService('reporting'); + const esArchiver = getService('esArchiver'); + const security = getService('security'); + + describe('Reporting', () => { + const createReportingUser = async () => { + await security.user.create(reporting.REPORTING_USER_USERNAME, { + password: reporting.REPORTING_USER_PASSWORD, + roles: ['reporting_user', 'data_analyst', 'kibana_user'], // Deprecated: using built-in `reporting_user` role grants all Reporting privileges + full_name: 'a reporting user', + }); + }; + + const deleteReportingUser = async () => { + await security.user.delete(reporting.REPORTING_USER_USERNAME); + }; + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/reporting/logs'); + await esArchiver.load('x-pack/test/functional/es_archives/logstash_functional'); + + await createReportingUser(); + await reporting.loginReportingUser(); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/reporting/logs'); + await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); + + await deleteReportingUser(); + }); + + beforeEach(async () => { + // Add one report + await supertestWithoutAuth + .post(`/api/reporting/generate/csv`) + .auth(reporting.REPORTING_USER_USERNAME, reporting.REPORTING_USER_PASSWORD) + .set('kbn-xsrf', 'xxx') + .send({ jobParams: JOB_PARAMS_RISON_CSV_DEPRECATED }) + .expect(200); + + await retry.waitFor('Reporting app', async () => { + await common.navigateToApp('reporting'); + return testSubjects.exists('reportingPageHeader'); + }); + }); + + afterEach(async () => { + await reporting.deleteAllReports(); + }); + + it('List reports view', async () => { + await retry.waitForWithTimeout('A reporting list item', 5000, () => { + return testSubjects.exists('reportingListItemObjectTitle'); + }); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 81cfd70a239562..e79bbdb86a88af 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -37,6 +37,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/security_solution'), require.resolve('./apps/ml_embeddables_in_dashboard'), require.resolve('./apps/remote_clusters'), + require.resolve('./apps/reporting'), ], pageObjects, diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 99293c71676b47..273db212400abd 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -9,6 +9,7 @@ import { services as kibanaFunctionalServices } from '../../../../test/functiona import { services as kibanaApiIntegrationServices } from '../../../../test/api_integration/services'; import { services as kibanaXPackApiIntegrationServices } from '../../api_integration/services'; import { services as commonServices } from '../../common/services'; +import { ReportingFunctionalProvider } from '../../reporting_functional/services'; import { MonitoringNoDataProvider, @@ -107,5 +108,6 @@ export const services = { dashboardDrilldownPanelActions: DashboardDrilldownPanelActionsProvider, dashboardDrilldownsManage: DashboardDrilldownsManageProvider, dashboardPanelTimeRange: DashboardPanelTimeRangeProvider, + reporting: ReportingFunctionalProvider, searchSessions: SearchSessionsService, }; diff --git a/x-pack/test/reporting_api_integration/reporting_without_security/ilm_migration_apis.ts b/x-pack/test/reporting_api_integration/reporting_without_security/ilm_migration_apis.ts index a0f4a3f91fe32a..a9b6798a0224fe 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security/ilm_migration_apis.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security/ilm_migration_apis.ts @@ -47,7 +47,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('detects when reporting indices should be migrated due to missing ILM policy', async () => { - await reportingAPI.makeAllReportingPoliciesUnmanaged(); + await reportingAPI.makeAllReportingIndicesUnmanaged(); // TODO: Remove "any" when no longer through type issue "policy_id" missing await es.ilm.deleteLifecycle({ policy: ILM_POLICY_NAME } as any); @@ -63,7 +63,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('detects when reporting indices should be migrated due to unmanaged indices', async () => { - await reportingAPI.makeAllReportingPoliciesUnmanaged(); + await reportingAPI.makeAllReportingIndicesUnmanaged(); await supertestNoAuth .post(`/api/reporting/generate/csv`) .set('kbn-xsrf', 'xxx') diff --git a/x-pack/test/reporting_api_integration/services/scenarios.ts b/x-pack/test/reporting_api_integration/services/scenarios.ts index eb32de9d0dc9ce..08c07e0e257edc 100644 --- a/x-pack/test/reporting_api_integration/services/scenarios.ts +++ b/x-pack/test/reporting_api_integration/services/scenarios.ts @@ -181,8 +181,8 @@ export function createScenarios({ getService }: Pick { - log.debug('ReportingAPI.makeAllReportingPoliciesUnmanaged'); + const makeAllReportingIndicesUnmanaged = async () => { + log.debug('ReportingAPI.makeAllReportingIndicesUnmanaged'); const settings: any = { 'index.lifecycle.name': null, }; @@ -214,6 +214,6 @@ export function createScenarios({ getService }: Pick Date: Fri, 9 Jul 2021 16:18:38 +0300 Subject: [PATCH 39/77] [Alerting UI] Fixed bug when rule state was updated on Edit flyout opening: Index action set empty documents property for non history index. (#105014) --- .../builtin_action_types/es_index/es_index_params.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx index 5d526e74564c5c..56f333396908bb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx @@ -43,6 +43,7 @@ export const IndexParamsFields = ({ ALERT_HISTORY_PREFIX, '' ); + const [isActionConnectorChanged, setIsActionConnectorChanged] = useState(false); const getDocumentToIndex = (doc: Array> | undefined) => doc && doc.length > 0 ? ((doc[0] as unknown) as string) : undefined; @@ -67,11 +68,12 @@ export const IndexParamsFields = ({ setUsePreconfiguredSchema(true); editAction('documents', [JSON.stringify(AlertHistoryDocumentTemplate)], index); setDocumentToIndex(JSON.stringify(AlertHistoryDocumentTemplate)); - } else { + } else if (isActionConnectorChanged) { setUsePreconfiguredSchema(false); editAction('documents', undefined, index); setDocumentToIndex(undefined); } + setIsActionConnectorChanged(true); // eslint-disable-next-line react-hooks/exhaustive-deps }, [actionConnector?.id]); From 4806cf3fd6055ab0be9cec6c9711ad8104bf49d6 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Fri, 9 Jul 2021 16:46:56 +0300 Subject: [PATCH 40/77] [TSVB] Table view - fix display of item urls (#105051) --- .../components/panel_config/table.tsx | 1 + .../components/vis_types/table/vis.js | 8 ++++++-- test/functional/apps/visualize/_tsvb_table.ts | 17 ++++++++++++++++- .../page_objects/visual_builder_page.ts | 7 +++++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx index 9ba0822402562e..3633f8add74578 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx @@ -207,6 +207,7 @@ export class TablePanelConfig extends Component< diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js index 4dd8f672c9ea30..4db038de912f5c 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js @@ -58,7 +58,11 @@ class TableVis extends Component { renderRow = (row) => { const { model } = this.props; - let rowDisplay = model.pivot_type === 'date' ? this.dateFormatter.convert(row.key) : row.key; + + let rowDisplay = getValueOrEmpty( + model.pivot_type === 'date' ? this.dateFormatter.convert(row.key) : row.key + ); + if (model.drilldown_url) { const url = replaceVars(model.drilldown_url, {}, { key: row.key }); rowDisplay = {rowDisplay}; @@ -98,7 +102,7 @@ class TableVis extends Component { }); return ( - {getValueOrEmpty(rowDisplay)} + {rowDisplay} {columns} ); diff --git a/test/functional/apps/visualize/_tsvb_table.ts b/test/functional/apps/visualize/_tsvb_table.ts index abe3b799e4711f..de0771d3c8ec55 100644 --- a/test/functional/apps/visualize/_tsvb_table.ts +++ b/test/functional/apps/visualize/_tsvb_table.ts @@ -10,12 +10,14 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getPageObjects }: FtrProviderContext) { +export default function ({ getPageObjects, getService }: FtrProviderContext) { const { visualBuilder, visualize, visChart } = getPageObjects([ 'visualBuilder', 'visualize', 'visChart', ]); + const findService = getService('find'); + const retry = getService('retry'); describe('visual builder', function describeIndexTests() { before(async () => { @@ -43,6 +45,19 @@ export default function ({ getPageObjects }: FtrProviderContext) { expect(tableData).to.be(EXPECTED); }); + it('should display drilldown urls', async () => { + const baseURL = 'http://elastic.co/foo/'; + + await visualBuilder.clickPanelOptions('table'); + await visualBuilder.setDrilldownUrl(`${baseURL}{{key}}`); + + await retry.try(async () => { + const links = await findService.allByCssSelector(`a[href="${baseURL}ios"]`); + + expect(links.length).to.be(1); + }); + }); + it('should display correct values on changing metrics aggregation', async () => { const EXPECTED = 'OS Cardinality\nwin 8 12\nwin xp 9\nwin 7 8\nios 5\nosx 3'; diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index ea11560e37b6ff..0f6c09f6ee4640 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -277,6 +277,13 @@ export class VisualBuilderPageObject extends FtrService { await this.comboBox.setElement(formatterEl, formatter, { clickWithMouse: true }); } + public async setDrilldownUrl(value: string) { + const drilldownEl = await this.testSubjects.find('drilldownUrl'); + + await drilldownEl.clearValue(); + await drilldownEl.type(value); + } + /** * set duration formatter additional settings * From ef991b7c2b93c53c1ccf1db0a3ba7506fa549f23 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Fri, 9 Jul 2021 16:53:48 +0300 Subject: [PATCH 41/77] [TSVB] fix include/exclude fields appear to migrated TSVB visualization when using Group by Terms (#104848) * [TSVB] Include/exclude fields appear to migrated TSVB visualization when using Group by Terms Closes: #104829 * add functional test * fix JEST --- .../components/aggs/cumulative_sum.js | 3 ++- .../application/components/aggs/derivative.js | 3 ++- .../components/aggs/moving_average.js | 3 ++- .../components/aggs/positive_only.js | 3 ++- .../components/aggs/serial_diff.js | 3 ++- .../components/aggs/std_sibling.js | 3 ++- .../application/components/aggs/vars.js | 3 ++- .../splits/__snapshots__/terms.test.js.snap | 2 ++ .../application/components/splits/terms.js | 8 +++++-- .../apps/visualize/_tsvb_time_series.ts | 5 ++++- .../page_objects/visual_builder_page.ts | 21 ++++++++++++++++++- 11 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js index f167bc35c06e99..a232a1dc03ae35 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js @@ -23,6 +23,7 @@ import { EuiFormRow, EuiSpacer, } from '@elastic/eui'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; export function CumulativeSumAgg(props) { const { model, siblings, fields, indexPattern } = props; @@ -70,7 +71,7 @@ export function CumulativeSumAgg(props) { onChange={handleSelectChange('field')} metrics={siblings} metric={model} - fields={fields[indexPattern]} + fields={fields[getIndexPatternKey(indexPattern)]} value={model.field} exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js index 9bed7015b0245c..616f40128ff228 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js @@ -25,6 +25,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; export const DerivativeAgg = (props) => { const { siblings, fields, indexPattern } = props; @@ -80,7 +81,7 @@ export const DerivativeAgg = (props) => { onChange={handleSelectChange('field')} metrics={siblings} metric={model} - fields={fields[indexPattern]} + fields={fields[getIndexPatternKey(indexPattern)]} value={model.field} exclude={[METRIC_TYPES.TOP_HIT]} fullWidth diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js index 79f70f45d6256c..a3ce43f97a36ae 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js @@ -26,6 +26,7 @@ import { EuiFieldNumber, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; const DEFAULTS = { model_type: MODEL_TYPES.UNWEIGHTED, @@ -141,7 +142,7 @@ export const MovingAverageAgg = (props) => { onChange={handleSelectChange('field')} metrics={siblings} metric={model} - fields={fields[indexPattern]} + fields={fields[getIndexPatternKey(indexPattern)]} value={model.field} exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js index 156a042abb4e20..c974f5d5f05f53 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js @@ -23,6 +23,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; export const PositiveOnlyAgg = (props) => { const { siblings, fields, indexPattern } = props; @@ -74,7 +75,7 @@ export const PositiveOnlyAgg = (props) => { onChange={handleSelectChange('field')} metrics={siblings} metric={model} - fields={fields[indexPattern]} + fields={fields[getIndexPatternKey(indexPattern)]} value={model.field} exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js index a553b1a4c6671b..efc2a72c3dd676 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js @@ -24,6 +24,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; export const SerialDiffAgg = (props) => { const { siblings, fields, indexPattern, model } = props; @@ -74,7 +75,7 @@ export const SerialDiffAgg = (props) => { onChange={handleSelectChange('field')} metrics={siblings} metric={model} - fields={fields[indexPattern]} + fields={fields[getIndexPatternKey(indexPattern)]} value={model.field} exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js index 9a30988d252e5e..d2b3f45a70164b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js @@ -27,6 +27,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; const StandardSiblingAggUi = (props) => { const { siblings, intl, fields, indexPattern } = props; @@ -147,7 +148,7 @@ const StandardSiblingAggUi = (props) => { onChange={handleSelectChange('field')} exclude={[METRIC_TYPES.PERCENTILE, METRIC_TYPES.TOP_HIT]} metrics={siblings} - fields={fields[indexPattern]} + fields={fields[getIndexPatternKey(indexPattern)]} metric={model} value={model.field} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js index b9d554e254bcce..ba06b0fffd3075 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js @@ -15,6 +15,7 @@ import { AddDeleteButtons } from '../add_delete_buttons'; import { collectionActions } from '../lib/collection_actions'; import { MetricSelect } from './metric_select'; import { EuiFlexGroup, EuiFlexItem, EuiFieldText } from '@elastic/eui'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; export const newVariable = (opts) => ({ id: uuid.v1(), name: '', field: '', ...opts }); @@ -59,7 +60,7 @@ export class CalculationVars extends Component { metrics={this.props.metrics} metric={this.props.model} value={row.field} - fields={this.props.fields[this.props.indexPattern]} + fields={this.props.fields[getIndexPatternKey(this.props.indexPattern)]} includeSiblings={this.props.includeSiblings} exclude={this.props.exclude} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap index 562c463f6c83cc..ce381a0e539d0d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap @@ -78,6 +78,7 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js labelType="label" > @@ -100,6 +101,7 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js labelType="label" > diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js index 7db6a75e2392c9..9c097de38d56ac 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js @@ -27,6 +27,7 @@ import { import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { KBN_FIELD_TYPES } from '../../../../../data/public'; import { STACKED_OPTIONS } from '../../visualizations/constants'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; const DEFAULTS = { terms_direction: 'desc', terms_size: 10, terms_order_by: '_count' }; @@ -75,10 +76,11 @@ export const SplitByTermsUI = ({ }), }, ]; + const fieldsSelector = getIndexPatternKey(indexPattern); const selectedDirectionOption = dirOptions.find((option) => { return model.terms_direction === option.value; }); - const selectedField = find(fields[indexPattern], ({ name }) => name === model.terms_field); + const selectedField = find(fields[fieldsSelector], ({ name }) => name === model.terms_field); const selectedFieldType = get(selectedField, 'type'); if ( @@ -144,6 +146,7 @@ export const SplitByTermsUI = ({ @@ -160,6 +163,7 @@ export const SplitByTermsUI = ({ @@ -198,7 +202,7 @@ export const SplitByTermsUI = ({ metrics={metrics} clearable={false} additionalOptions={[defaultCount, terms]} - fields={fields[indexPattern]} + fields={fields[fieldsSelector]} onChange={handleSelectChange('terms_order_by')} restrict="basic" value={model.terms_order_by} diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts index a0c9d806facc6f..cc57d583481806 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/_tsvb_time_series.ts @@ -155,7 +155,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Clicking on the chart', () => { it(`should create a filter`, async () => { - await visualBuilder.setMetricsGroupByTerms('machine.os.raw'); + await visualBuilder.setMetricsGroupByTerms('machine.os.raw', { + include: 'win 7', + exclude: 'ios', + }); await visualBuilder.clickSeriesOption(); await testSubjects.click('visualizeSaveButton'); diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 0f6c09f6ee4640..8e28ffab6c9c3d 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -635,7 +635,10 @@ export class VisualBuilderPageObject extends FtrService { return await this.find.allByCssSelector('.tvbSeriesEditor'); } - public async setMetricsGroupByTerms(field: string) { + public async setMetricsGroupByTerms( + field: string, + filtering: { include?: string; exclude?: string } = {} + ) { const groupBy = await this.find.byCssSelector( '.tvbAggRow--split [data-test-subj="comboBoxInput"]' ); @@ -643,6 +646,22 @@ export class VisualBuilderPageObject extends FtrService { await this.common.sleep(1000); const byField = await this.testSubjects.find('groupByField'); await this.comboBox.setElement(byField, field); + + await this.setMetricsGroupByFiltering(filtering.include, filtering.exclude); + } + + public async setMetricsGroupByFiltering(include?: string, exclude?: string) { + const setFilterValue = async (value: string | undefined, subjectKey: string) => { + if (typeof value === 'string') { + const valueSubject = await this.testSubjects.find(subjectKey); + + await valueSubject.clearValue(); + await valueSubject.type(value); + } + }; + + await setFilterValue(include, 'groupByInclude'); + await setFilterValue(exclude, 'groupByExclude'); } public async checkSelectedMetricsGroupByValue(value: string) { From efa4ce22faa06a7a0df288ead1e4b905939fe416 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Fri, 9 Jul 2021 08:54:48 -0500 Subject: [PATCH 42/77] Storybook EUI theme decorator (#103582) Add an `EuiThemeProviderDecorator` to kibanaReact which uses the Storybook globals to set the EUI theme. Add global decorators to the APM and Observability plugins so all stories are wrapped in the `EuiThemeProviderDecorator`, and they don't need to specify it in the stories. Add [jest setup helpers recommended by @storybook/testing-react](https://github.com/storybookjs/testing-react#global-config). --- .../common/eui_styled_components.tsx | 17 +++- x-pack/plugins/apm/.storybook/jest_setup.js | 11 +++ x-pack/plugins/apm/.storybook/preview.js | 10 +++ x-pack/plugins/apm/jest.config.js | 2 + .../index.stories.tsx | 23 ++--- .../__stories__/MapTooltip.stories.tsx | 85 +++++++++---------- .../index.stories.tsx | 14 ++- .../Distribution/index.stories.tsx | 13 ++- .../exception_stacktrace.stories.tsx | 8 -- .../service_map/Popover/Popover.stories.tsx | 17 ++-- .../Popover/service_stats_list.stories.tsx | 10 +-- .../__stories__/Cytoscape.stories.tsx | 10 +-- .../cytoscape_example_data.stories.tsx | 10 +-- .../WaterfallContainer.stories.tsx | 9 +- .../shared/agent_icon/agent_icon.stories.tsx | 18 ++-- .../custom_tooltip.stories.tsx | 10 +-- ...ces_latency_distribution_chart.stories.tsx | 10 +-- .../latency_chart/latency_chart.stories.tsx | 27 +++--- .../shared/span_icon/span_icon.stories.tsx | 17 +--- .../observability/.storybook/jest_setup.js | 11 +++ .../observability/.storybook/preview.js | 10 +++ x-pack/plugins/observability/jest.config.js | 1 + .../__stories__/core_vitals.stories.tsx | 5 +- .../field_value_selection.stories.tsx | 19 ++--- .../public/pages/landing/landing.stories.tsx | 5 +- .../pages/overview/overview.stories.tsx | 5 +- 26 files changed, 165 insertions(+), 212 deletions(-) create mode 100644 x-pack/plugins/apm/.storybook/jest_setup.js create mode 100644 x-pack/plugins/apm/.storybook/preview.js create mode 100644 x-pack/plugins/observability/.storybook/jest_setup.js create mode 100644 x-pack/plugins/observability/.storybook/preview.js diff --git a/src/plugins/kibana_react/common/eui_styled_components.tsx b/src/plugins/kibana_react/common/eui_styled_components.tsx index 10cd168da6faa6..62876a03c7d831 100644 --- a/src/plugins/kibana_react/common/eui_styled_components.tsx +++ b/src/plugins/kibana_react/common/eui_styled_components.tsx @@ -6,15 +6,14 @@ * Side Public License, v 1. */ +import type { DecoratorFn } from '@storybook/react'; import React from 'react'; import * as styledComponents from 'styled-components'; import { ThemedStyledComponentsModule, ThemeProvider, ThemeProviderProps } from 'styled-components'; - -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiThemeVars, euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps/theme'; export interface EuiTheme { - eui: typeof euiLightVars | typeof euiDarkVars; + eui: typeof euiThemeVars; darkMode: boolean; } @@ -36,6 +35,16 @@ const EuiThemeProvider = < /> ); +/** + * Storybook decorator using the EUI theme provider. Uses the value from + * `globals` provided by the Storybook theme switcher. + */ +export const EuiThemeProviderDecorator: DecoratorFn = (storyFn, { globals }) => { + const darkMode = globals.euiTheme === 'v8.dark' || globals.euiTheme === 'v7.dark'; + + return {storyFn()}; +}; + const { default: euiStyled, css, diff --git a/x-pack/plugins/apm/.storybook/jest_setup.js b/x-pack/plugins/apm/.storybook/jest_setup.js new file mode 100644 index 00000000000000..32071b8aa3f628 --- /dev/null +++ b/x-pack/plugins/apm/.storybook/jest_setup.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setGlobalConfig } from '@storybook/testing-react'; +import * as globalStorybookConfig from './preview'; + +setGlobalConfig(globalStorybookConfig); diff --git a/x-pack/plugins/apm/.storybook/preview.js b/x-pack/plugins/apm/.storybook/preview.js new file mode 100644 index 00000000000000..18343c15a6465c --- /dev/null +++ b/x-pack/plugins/apm/.storybook/preview.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiThemeProviderDecorator } from '../../../../src/plugins/kibana_react/common'; + +export const decorators = [EuiThemeProviderDecorator]; diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js index caa8256cdb7eae..5bce9bbfb5b1be 100644 --- a/x-pack/plugins/apm/jest.config.js +++ b/x-pack/plugins/apm/jest.config.js @@ -11,4 +11,6 @@ module.exports = { preset: '@kbn/test', rootDir: path.resolve(__dirname, '../../..'), roots: ['/x-pack/plugins/apm'], + setupFiles: ['/x-pack/plugins/apm/.storybook/jest_setup.js'], + testPathIgnorePatterns: ['/x-pack/plugins/apm/e2e/'], }; diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx index 83874e95845104..23afb9646dea75 100644 --- a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { ErrorCountAlertTrigger } from '.'; -import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, @@ -20,19 +19,15 @@ export default { component: ErrorCountAlertTrigger, decorators: [ (Story: React.ComponentClass) => ( - - - -
- -
-
-
-
+ + +
+ +
+
+
), ], }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx index 1aad25fc89c0b1..8263db648cd398 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx @@ -7,53 +7,50 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; -import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; import { MapToolTip } from '../MapToolTip'; import { COUNTRY_NAME, TRANSACTION_DURATION_COUNTRY } from '../useLayerList'; -storiesOf('app/RumDashboard/VisitorsRegionMap', module) - .addDecorator((storyFn) => {storyFn()}) - .add( - 'Tooltip', - () => { - const loadFeatureProps = async () => { - return [ +storiesOf('app/RumDashboard/VisitorsRegionMap', module).add( + 'Tooltip', + () => { + const loadFeatureProps = async () => { + return [ + { + getPropertyKey: () => COUNTRY_NAME, + getRawValue: () => 'United States', + }, + { + getPropertyKey: () => TRANSACTION_DURATION_COUNTRY, + getRawValue: () => 2434353, + }, + ]; + }; + return ( + COUNTRY_NAME, - getRawValue: () => 'United States', - }, - { - getPropertyKey: () => TRANSACTION_DURATION_COUNTRY, - getRawValue: () => 2434353, - }, - ]; - }; - return ( - - ); + actions: [], + }, + ]} + /> + ); + }, + { + info: { + propTables: false, + source: false, }, - { - info: { - propTables: false, - source: false, - }, - } - ); + } +); diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/index.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/index.stories.tsx index cd5fa5db89a31c..02ecf902f00a31 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/index.stories.tsx @@ -8,7 +8,6 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; import { CoreStart } from 'kibana/public'; -import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; import { AgentConfiguration } from '../../../../../../common/agent_configuration/configuration_types'; import { FETCH_STATUS } from '../../../../../hooks/use_fetcher'; import { createCallApmApi } from '../../../../../services/rest/createCallApmApi'; @@ -37,13 +36,11 @@ storiesOf( }; return ( - - - {storyFn()} - - + + {storyFn()} + ); }) .add( @@ -67,7 +64,6 @@ storiesOf( propTablesExclude: [ AgentConfigurationCreateEdit, ApmPluginContext.Provider, - EuiThemeProvider, ], source: false, }, diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.stories.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.stories.tsx index 8cc16dd801c25d..d434a155c9cf45 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.stories.tsx @@ -11,7 +11,6 @@ import { ApmPluginContext, ApmPluginContextValue, } from '../../../../context/apm_plugin/apm_plugin_context'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { ErrorDistribution } from './'; export default { @@ -28,13 +27,11 @@ export default { }; return ( - - - - - - - + + + + + ); }, ], diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/exception_stacktrace.stories.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/exception_stacktrace.stories.tsx index f21c189584d31c..9468202edf4d6b 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/exception_stacktrace.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/exception_stacktrace.stories.tsx @@ -7,7 +7,6 @@ import { Story } from '@storybook/react'; import React, { ComponentProps, ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { ExceptionStacktrace } from './exception_stacktrace'; type Args = ComponentProps; @@ -15,13 +14,6 @@ type Args = ComponentProps; export default { title: 'app/ErrorGroupDetails/DetailView/ExceptionStacktrace', component: ExceptionStacktrace, - decorators: [ - (StoryComponent: ComponentType) => ( - - - - ), - ], }; export const JavaWithLongLines: Story = (args) => ( diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/Popover.stories.tsx index 6b7626514d03fb..324a38ea5db39d 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/Popover.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/Popover.stories.tsx @@ -8,7 +8,6 @@ import cytoscape from 'cytoscape'; import { CoreStart } from 'kibana/public'; import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider'; import { createCallApmApi } from '../../../../services/rest/createCallApmApi'; @@ -38,15 +37,13 @@ export default { createCallApmApi(coreMock); return ( - - - -
- -
-
-
-
+ + +
+ +
+
+
); }, ], diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_stats_list.stories.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_stats_list.stories.tsx index a8f004a7295d90..f1a89043f826e1 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_stats_list.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_stats_list.stories.tsx @@ -5,20 +5,12 @@ * 2.0. */ -import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; +import React from 'react'; import { ServiceStatsList } from './ServiceStatsList'; export default { title: 'app/ServiceMap/Popover/ServiceStatsList', component: ServiceStatsList, - decorators: [ - (Story: ComponentType) => ( - - - - ), - ], }; export function Example() { diff --git a/x-pack/plugins/apm/public/components/app/service_map/__stories__/Cytoscape.stories.tsx b/x-pack/plugins/apm/public/components/app/service_map/__stories__/Cytoscape.stories.tsx index 8bc0d7239e9c52..7ce9c3e9436132 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/__stories__/Cytoscape.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/__stories__/Cytoscape.stories.tsx @@ -6,21 +6,13 @@ */ import cytoscape from 'cytoscape'; -import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; +import React from 'react'; import { Cytoscape } from '../Cytoscape'; import { Centerer } from './centerer'; export default { title: 'app/ServiceMap/Cytoscape', component: Cytoscape, - decorators: [ - (Story: ComponentType) => ( - - - - ), - ], }; export function Example() { diff --git a/x-pack/plugins/apm/public/components/app/service_map/__stories__/cytoscape_example_data.stories.tsx b/x-pack/plugins/apm/public/components/app/service_map/__stories__/cytoscape_example_data.stories.tsx index 45de632a152d41..192447ef7591ad 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/__stories__/cytoscape_example_data.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/__stories__/cytoscape_example_data.stories.tsx @@ -16,8 +16,7 @@ import { EuiSpacer, EuiToolTip, } from '@elastic/eui'; -import React, { ComponentType, useEffect, useState } from 'react'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; +import React, { useEffect, useState } from 'react'; import { Cytoscape } from '../Cytoscape'; import { Centerer } from './centerer'; import exampleResponseHipsterStore from './example_response_hipster_store.json'; @@ -42,13 +41,6 @@ function getHeight() { export default { title: 'app/ServiceMap/Example data', component: Cytoscape, - decorators: [ - (Story: ComponentType) => ( - - - - ), - ], }; export function GenerateMap() { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx index 5ea2fca2dfa321..20ca3194fbfdf2 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx @@ -7,7 +7,6 @@ import React, { ComponentType } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { TraceAPIResponse } from '../../../../../../server/lib/traces/get_trace'; import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; @@ -27,11 +26,9 @@ export default { decorators: [ (Story: ComponentType) => ( - - - - - + + + ), ], diff --git a/x-pack/plugins/apm/public/components/shared/agent_icon/agent_icon.stories.tsx b/x-pack/plugins/apm/public/components/shared/agent_icon/agent_icon.stories.tsx index bc41fd58ea5d24..68c3edabfa44ef 100644 --- a/x-pack/plugins/apm/public/components/shared/agent_icon/agent_icon.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/agent_icon/agent_icon.stories.tsx @@ -7,30 +7,22 @@ import { EuiCard, + EuiCodeBlock, EuiFlexGroup, - EuiImage, EuiFlexItem, + EuiImage, EuiSpacer, EuiToolTip, - EuiCodeBlock, } from '@elastic/eui'; -import React, { ComponentType } from 'react'; -import { AgentIcon } from './index'; -import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; +import React from 'react'; import { AGENT_NAMES } from '../../../../common/agent_name'; -import { getAgentIcon } from './get_agent_icon'; import { useTheme } from '../../../hooks/use_theme'; +import { getAgentIcon } from './get_agent_icon'; +import { AgentIcon } from './index'; export default { title: 'shared/icons', component: AgentIcon, - decorators: [ - (Story: ComponentType) => ( - - - - ), - ], }; export function AgentIcons() { diff --git a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/custom_tooltip.stories.tsx b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/custom_tooltip.stories.tsx index 0eb5b0e84ff39f..6128526c577e4e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/custom_tooltip.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/custom_tooltip.stories.tsx @@ -6,8 +6,7 @@ */ import { TooltipInfo } from '@elastic/charts'; -import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; +import React from 'react'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; import { MainStatsServiceInstanceItem } from '../../../app/service_overview/service_overview_instances_chart_and_table'; import { CustomTooltip } from './custom_tooltip'; @@ -25,13 +24,6 @@ function getLatencyFormatter(props: TooltipInfo) { export default { title: 'shared/charts/InstancesLatencyDistributionChart/CustomTooltip', component: CustomTooltip, - decorators: [ - (Story: ComponentType) => ( - - - - ), - ], }; export function Example(props: TooltipInfo) { diff --git a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/instances_latency_distribution_chart.stories.tsx b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/instances_latency_distribution_chart.stories.tsx index c574645d485d53..80bfcca05aabcb 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/instances_latency_distribution_chart.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/instances_latency_distribution_chart.stories.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; +import React from 'react'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { InstancesLatencyDistributionChart, @@ -16,13 +15,6 @@ import { export default { title: 'shared/charts/InstancesLatencyDistributionChart', component: InstancesLatencyDistributionChart, - decorators: [ - (Story: ComponentType) => ( - - - - ), - ], }; export function Example({ items }: InstancesLatencyDistributionChartProps) { diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx index d1dcd831eadd7d..ff2b95667a63a8 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx @@ -8,7 +8,6 @@ import { StoryContext } from '@storybook/react'; import React, { ComponentType } from 'react'; import { MemoryRouter, Route } from 'react-router-dom'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; import { @@ -76,20 +75,18 @@ export default { - - - - - - - - - + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx b/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx index b053f441e9632b..7d2e2fbefc3590 100644 --- a/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx @@ -6,32 +6,23 @@ */ import { - EuiImage, EuiCard, + EuiCodeBlock, EuiFlexGroup, EuiFlexItem, + EuiImage, EuiSpacer, - EuiCodeBlock, EuiToolTip, } from '@elastic/eui'; -import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; +import React from 'react'; +import { getSpanIcon, spanTypeIcons } from './get_span_icon'; import { SpanIcon } from './index'; -import { getSpanIcon } from './get_span_icon'; -import { spanTypeIcons } from './get_span_icon'; const spanTypes = Object.keys(spanTypeIcons); export default { title: 'shared/icons', component: SpanIcon, - decorators: [ - (Story: ComponentType) => ( - - - - ), - ], }; export function SpanIcons() { diff --git a/x-pack/plugins/observability/.storybook/jest_setup.js b/x-pack/plugins/observability/.storybook/jest_setup.js new file mode 100644 index 00000000000000..32071b8aa3f628 --- /dev/null +++ b/x-pack/plugins/observability/.storybook/jest_setup.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setGlobalConfig } from '@storybook/testing-react'; +import * as globalStorybookConfig from './preview'; + +setGlobalConfig(globalStorybookConfig); diff --git a/x-pack/plugins/observability/.storybook/preview.js b/x-pack/plugins/observability/.storybook/preview.js new file mode 100644 index 00000000000000..18343c15a6465c --- /dev/null +++ b/x-pack/plugins/observability/.storybook/preview.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiThemeProviderDecorator } from '../../../../src/plugins/kibana_react/common'; + +export const decorators = [EuiThemeProviderDecorator]; diff --git a/x-pack/plugins/observability/jest.config.js b/x-pack/plugins/observability/jest.config.js index 66d42122382f37..6fdeab06df0530 100644 --- a/x-pack/plugins/observability/jest.config.js +++ b/x-pack/plugins/observability/jest.config.js @@ -9,4 +9,5 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/observability'], + setupFiles: ['/x-pack/plugins/observability/.storybook/jest_setup.js'], }; diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx index 5f5cf2cb4da217..5c07b4626cf19e 100644 --- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx @@ -9,7 +9,6 @@ import React, { ComponentType } from 'react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { Observable } from 'rxjs'; import { CoreStart } from 'src/core/public'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { createKibanaReactContext } from '../../../../../../../../src/plugins/kibana_react/public'; import { CoreVitalItem } from '../core_vital_item'; import { LCP_HELP_LABEL, LCP_LABEL } from '../translations'; @@ -25,9 +24,7 @@ export default { (Story: ComponentType) => ( - - - + ), diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/__stories__/field_value_selection.stories.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/__stories__/field_value_selection.stories.tsx index 80a25b82eb8cb4..1152ba32960ed9 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/__stories__/field_value_selection.stories.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/__stories__/field_value_selection.stories.tsx @@ -10,7 +10,6 @@ import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { Observable } from 'rxjs'; import { CoreStart } from 'src/core/public'; import { text } from '@storybook/addon-knobs'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { createKibanaReactContext } from '../../../../../../../../src/plugins/kibana_react/public'; import { FieldValueSelectionProps } from '../types'; import { FieldValueSelection } from '../field_value_selection'; @@ -31,16 +30,14 @@ export default { (Story: ComponentType) => ( - - {}} - selectedValue={[]} - loading={false} - setQuery={() => {}} - /> - + {}} + selectedValue={[]} + loading={false} + setQuery={() => {}} + /> ), diff --git a/x-pack/plugins/observability/public/pages/landing/landing.stories.tsx b/x-pack/plugins/observability/public/pages/landing/landing.stories.tsx index 86922b045c7429..ef3ded61492c7b 100644 --- a/x-pack/plugins/observability/public/pages/landing/landing.stories.tsx +++ b/x-pack/plugins/observability/public/pages/landing/landing.stories.tsx @@ -6,7 +6,6 @@ */ import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; import { PluginContext, PluginContextValue } from '../../context/plugin_context'; import { LandingPage } from './'; @@ -27,9 +26,7 @@ export default { } as unknown) as PluginContextValue; return ( - - - + ); }, diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx index dd424cf221d15f..29823332353315 100644 --- a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx +++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx @@ -10,7 +10,6 @@ import { storiesOf } from '@storybook/react'; import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; import { HasDataContextProvider } from '../../context/has_data_context'; import { PluginContext } from '../../context/plugin_context'; @@ -65,9 +64,7 @@ const withCore = makeDecorator({ ObservabilityPageTemplate: KibanaPageTemplate, }} > - - {storyFn(context)} - + {storyFn(context)} ); From 583b867d44b926fc1f19fa02f76bc2f83f99440b Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Fri, 9 Jul 2021 10:15:04 -0400 Subject: [PATCH 43/77] [RAC] [RBAC] Fix hyperlinks in typedocs for alerts client (#104975) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/rule_registry/docs/README.md | 2 +- .../alerts_client/classes/alertsclient.md | 32 +++++++++---------- .../interfaces/constructoroptions.md | 8 ++--- .../alerts_client/interfaces/updateoptions.md | 8 ++--- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/rule_registry/docs/README.md b/x-pack/plugins/rule_registry/docs/README.md index a22dc1ab7e8642..0eb24630051936 100644 --- a/x-pack/plugins/rule_registry/docs/README.md +++ b/x-pack/plugins/rule_registry/docs/README.md @@ -19,7 +19,7 @@ yarn global add typedoc typedoc-plugin-markdown ```bash cd x-pack/plugins/rule_registry/docs -npx typedoc --options alerts_client_typedoc.json +npx typedoc --gitRemote upstream --options alerts_client_typedoc.json ``` After running the above commands the files in the `server` directory will be updated to match the new tsdocs. diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md b/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md index 9b639829a9f5fa..359834bf9c2e71 100644 --- a/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md +++ b/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md @@ -41,7 +41,7 @@ on alerts as data. #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:59](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L59) +[rule_registry/server/alert_data_client/alerts_client.ts:59](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L59) ## Properties @@ -51,7 +51,7 @@ on alerts as data. #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:57](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L57) +[rule_registry/server/alert_data_client/alerts_client.ts:57](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L57) ___ @@ -61,7 +61,7 @@ ___ #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:58](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L58) +[rule_registry/server/alert_data_client/alerts_client.ts:58](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L58) ___ @@ -71,7 +71,7 @@ ___ #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:59](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L59) +[rule_registry/server/alert_data_client/alerts_client.ts:59](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L59) ___ @@ -81,13 +81,13 @@ ___ #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:56](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L56) +[rule_registry/server/alert_data_client/alerts_client.ts:56](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L56) ## Methods ### fetchAlert -▸ `Private` **fetchAlert**(`__namedParameters`): `Promise` +▸ `Private` **fetchAlert**(`__namedParameters`): `Promise`\>, ``"kibana.rac.alert.owner"`` \| ``"rule.id"``\> & { `kibana.rac.alert.owner`: `string` ; `rule.id`: `string` } & { `_version`: `undefined` \| `string` }\> #### Parameters @@ -97,17 +97,17 @@ ___ #### Returns -`Promise` +`Promise`\>, ``"kibana.rac.alert.owner"`` \| ``"rule.id"``\> & { `kibana.rac.alert.owner`: `string` ; `rule.id`: `string` } & { `_version`: `undefined` \| `string` }\> #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:79](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L79) +[rule_registry/server/alert_data_client/alerts_client.ts:79](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L79) ___ ### get -▸ **get**(`__namedParameters`): `Promise`\>\> +▸ **get**(`__namedParameters`): `Promise`\>\> #### Parameters @@ -117,11 +117,11 @@ ___ #### Returns -`Promise`\>\> +`Promise`\>\> #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:108](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L108) +[rule_registry/server/alert_data_client/alerts_client.ts:115](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L115) ___ @@ -142,7 +142,7 @@ ___ #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:68](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L68) +[rule_registry/server/alert_data_client/alerts_client.ts:68](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L68) ___ @@ -162,13 +162,13 @@ ___ #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:200](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L200) +[rule_registry/server/alert_data_client/alerts_client.ts:219](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L219) ___ ### update -▸ **update**(`__namedParameters`): `Promise`<`Object`\> +▸ **update**(`__namedParameters`): `Promise` #### Type parameters @@ -184,8 +184,8 @@ ___ #### Returns -`Promise`<`Object`\> +`Promise` #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:146](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L146) +[rule_registry/server/alert_data_client/alerts_client.ts:160](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L160) diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md index e3dbc6b2c2354a..051a5affc03799 100644 --- a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md +++ b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md @@ -19,7 +19,7 @@ #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:34](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L34) +[rule_registry/server/alert_data_client/alerts_client.ts:34](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L34) ___ @@ -29,7 +29,7 @@ ___ #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:33](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L33) +[rule_registry/server/alert_data_client/alerts_client.ts:33](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L33) ___ @@ -39,7 +39,7 @@ ___ #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:35](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L35) +[rule_registry/server/alert_data_client/alerts_client.ts:35](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L35) ___ @@ -49,4 +49,4 @@ ___ #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:32](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L32) +[rule_registry/server/alert_data_client/alerts_client.ts:32](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L32) diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md index fbc09916350008..10e793155c1964 100644 --- a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md +++ b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md @@ -25,7 +25,7 @@ #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:41](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L41) +[rule_registry/server/alert_data_client/alerts_client.ts:41](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L41) ___ @@ -35,7 +35,7 @@ ___ #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:39](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L39) +[rule_registry/server/alert_data_client/alerts_client.ts:39](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L39) ___ @@ -45,7 +45,7 @@ ___ #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:42](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L42) +[rule_registry/server/alert_data_client/alerts_client.ts:42](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L42) ___ @@ -55,4 +55,4 @@ ___ #### Defined in -[rule_registry/server/alert_data_client/alerts_client.ts:40](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L40) +[rule_registry/server/alert_data_client/alerts_client.ts:40](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L40) From 3cbea1bcaabeaaf3399047b3da12cc6533f12a47 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 9 Jul 2021 15:19:27 +0100 Subject: [PATCH 44/77] skip flaky suite (#105016) --- x-pack/test/functional/apps/lens/formula.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/lens/formula.ts b/x-pack/test/functional/apps/lens/formula.ts index 8b87db21a1ffeb..6148215d8b6d25 100644 --- a/x-pack/test/functional/apps/lens/formula.ts +++ b/x-pack/test/functional/apps/lens/formula.ts @@ -16,7 +16,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const fieldEditor = getService('fieldEditor'); - describe('lens formula', () => { + // FLAKY: https://github.com/elastic/kibana/issues/105016 + describe.skip('lens formula', () => { it('should transition from count to formula', async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.searchForItemWithName('lnsXYvis'); From 8c366faf8f5e729216ea4b5f9b4c2d774d664742 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Fri, 9 Jul 2021 18:32:34 +0300 Subject: [PATCH 45/77] Fix of the `ja-JP.json not found` error. (#105023) * Fixed `i18nrc not found ja-JP.json file` at expression_reveal_image. * Fixed `i18nrc not found ja-JP.json file` at `screenshotMode`. * Fixed `i18nrc not found ja-JP.json file` at `x-pack/plugins/timelines`. --- src/plugins/expression_reveal_image/.i18nrc.json | 7 ------- src/plugins/screenshot_mode/.i18nrc.json | 7 ------- x-pack/plugins/timelines/.i18nrc.json | 7 ------- 3 files changed, 21 deletions(-) delete mode 100755 src/plugins/expression_reveal_image/.i18nrc.json delete mode 100644 src/plugins/screenshot_mode/.i18nrc.json delete mode 100644 x-pack/plugins/timelines/.i18nrc.json diff --git a/src/plugins/expression_reveal_image/.i18nrc.json b/src/plugins/expression_reveal_image/.i18nrc.json deleted file mode 100755 index 5b073e4374519c..00000000000000 --- a/src/plugins/expression_reveal_image/.i18nrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "prefix": "expressionRevealImage", - "paths": { - "expressionRevealImage": "." - }, - "translations": ["translations/ja-JP.json"] -} diff --git a/src/plugins/screenshot_mode/.i18nrc.json b/src/plugins/screenshot_mode/.i18nrc.json deleted file mode 100644 index 79643fbb63d30d..00000000000000 --- a/src/plugins/screenshot_mode/.i18nrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "prefix": "screenshotMode", - "paths": { - "screenshotMode": "." - }, - "translations": ["translations/ja-JP.json"] -} diff --git a/x-pack/plugins/timelines/.i18nrc.json b/x-pack/plugins/timelines/.i18nrc.json deleted file mode 100644 index 4fe01ccc7bc694..00000000000000 --- a/x-pack/plugins/timelines/.i18nrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "prefix": "timelines", - "paths": { - "timelines": "." - }, - "translations": ["translations/ja-JP.json"] -} From dacb5949a2639932296347a09a14388c9e340af9 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Fri, 9 Jul 2021 18:03:52 +0200 Subject: [PATCH 46/77] Bump fast-safe-stringify to v2.0.8 (#105066) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index c64f4e1c4b2810..227301eed8dd6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13489,9 +13489,9 @@ fast-redact@^3.0.0: integrity sha512-a/S/Hp6aoIjx7EmugtzLqXmcNsyFszqbt6qQ99BdG61QjBZF6shNis0BYR6TsZOQ1twYc0FN2Xdhwwbv6+KD0w== fast-safe-stringify@2.x.x, fast-safe-stringify@^2.0.4, fast-safe-stringify@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" - integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== + version "2.0.8" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz#dc2af48c46cf712b683e849b2bbd446b32de936f" + integrity sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag== fast-shallow-equal@^1.0.0: version "1.0.0" From 48fa754042e7597b65b69b2c98fb2c6aa2057c4d Mon Sep 17 00:00:00 2001 From: Vadim Yakhin Date: Fri, 9 Jul 2021 13:11:14 -0300 Subject: [PATCH 47/77] Replace cmd with bash as EuiCodeBlock language (#105065) cmd is no longer supported by 3rd party library used by EuiCodeBlock --- .../components/shared/status_item/status_item.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/status_item/status_item.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/status_item/status_item.tsx index 79455ccc1d90d3..35ac8f1b85c05f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/status_item/status_item.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/status_item/status_item.tsx @@ -44,7 +44,7 @@ export const StatusItem: React.FC = ({ details }) => { const infoPopover = ( Date: Fri, 9 Jul 2021 09:23:18 -0700 Subject: [PATCH 48/77] [App Search] Relevance Tuning: Fix unsaved changes bug (#104951) * Fix unsavedChanges false positive when MultiInputRows is present - The fix for this is to change MultiInputRows from useEffect to useUpdateEffect, which prevents onChange from firing on initial mount/render (triggering updateBoostValue->unsavedChanges) @see https://github.com/streamich/react-use/blob/master/docs/useUpdateEffect.md * Fix precision tuner not triggering unsavedChanges --- .../multi_input_rows/multi_input_rows.test.tsx | 12 ++++++++++-- .../components/multi_input_rows/multi_input_rows.tsx | 5 +++-- .../relevance_tuning/relevance_tuning_logic.test.ts | 3 ++- .../relevance_tuning/relevance_tuning_logic.ts | 1 + 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/multi_input_rows/multi_input_rows.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/multi_input_rows/multi_input_rows.test.tsx index 3b8e1c96ff5040..63952bc2a6de77 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/multi_input_rows/multi_input_rows.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/multi_input_rows/multi_input_rows.test.tsx @@ -10,7 +10,7 @@ import '../../../__mocks__/shallow_useeffect.mock'; import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { rerender } from '../../../test_helpers'; @@ -162,10 +162,18 @@ describe('MultiInputRows', () => { }); describe('onChange', () => { + let wrapper: ShallowWrapper; const onChange = jest.fn(); + beforeEach(() => { + wrapper = shallow(); + }); + + it('does not call on change on mount', () => { + expect(onChange).not.toHaveBeenCalled(); + }); + it('returns the current values dynamically on change', () => { - const wrapper = shallow(); setMockValues({ ...values, values: ['updated'] }); rerender(wrapper); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/multi_input_rows/multi_input_rows.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/multi_input_rows/multi_input_rows.tsx index ac61e69eb44c4a..257f4b637f3e08 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/multi_input_rows/multi_input_rows.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/multi_input_rows/multi_input_rows.tsx @@ -5,9 +5,10 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React from 'react'; import { useValues, useActions } from 'kea'; +import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import { EuiForm, EuiButton, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; @@ -49,7 +50,7 @@ export const MultiInputRows: React.FC = ({ const { values, addedNewRow, hasEmptyValues, hasOnlyOneValue } = useValues(logic); const { addValue, editValue, deleteValue } = useActions(logic); - useEffect(() => { + useUpdateEffect(() => { if (onChange) { onChange(filterEmptyValues(values)); } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts index 4233a7b300d159..e2493b6404f7d3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts @@ -229,7 +229,7 @@ describe('RelevanceTuningLogic', () => { }); describe('updatePrecision', () => { - it('should set precision inside search settings', () => { + it('should set precision inside search settings and set unsavedChanges to true', () => { mount(); RelevanceTuningLogic.actions.updatePrecision(9); @@ -239,6 +239,7 @@ describe('RelevanceTuningLogic', () => { ...DEFAULT_VALUES.searchSettings, precision: 9, }, + unsavedChanges: true, }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts index 743bb1aa1502bd..02903b4588ed42 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts @@ -191,6 +191,7 @@ export const RelevanceTuningLogic = kea< unsavedChanges: [ false, { + updatePrecision: () => true, setSearchSettings: () => true, setSearchSettingsResponse: () => false, }, From 81f09a863de65a16d88bbc8763957190ae4165c8 Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Fri, 9 Jul 2021 18:24:18 +0200 Subject: [PATCH 49/77] [Security Solution] [Endpoint] Allow filtering activity log with date range (#104085) * use date range in search query fixes elastic/security-team/issues/1137 * make any date selection fetch matching log fixes elastic/security-team/issues/1137 * use a single action for updating paging info and fetching data fixes elastic/security-team/issues/1137 * use consistent types for some reason TS was complaining earlier with `undefined` * reset date picker on tab load fixes elastic/security-team/issues/1137 * refactor date pickers into a component refs elastic/security-team/issues/1137 * clear dates on change of endpoint fixes elastic/security-team/issues/1137 * do not show empty state if date filtering results return empty data fixes elastic/security-team/issues/1137 * add tests fixes elastic/security-team/issues/1137 * review changes * update comment refs f551b67d661a330621fb77c435db26ec90288b81 * store invalidDateRange on redux store and decouple logic from the component review changes * fix test * fix lint * review changes * expand date picker to use the full width of the flyout review changes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/endpoint/schema/actions.ts | 2 + .../common/endpoint/types/actions.ts | 2 + .../pages/endpoint_hosts/store/action.ts | 20 +-- .../pages/endpoint_hosts/store/builders.ts | 3 + .../pages/endpoint_hosts/store/index.test.ts | 1 + .../pages/endpoint_hosts/store/middleware.ts | 56 ++++++-- .../pages/endpoint_hosts/store/reducer.ts | 25 ++-- .../management/pages/endpoint_hosts/types.ts | 5 +- .../pages/endpoint_hosts/utils.test.ts | 30 +++++ .../management/pages/endpoint_hosts/utils.ts | 16 +++ .../activity_log_date_range_picker/index.tsx | 127 ++++++++++++++++++ .../components/endpoint_details_tabs.tsx | 13 +- .../view/details/endpoint_activity_log.tsx | 34 +++-- .../pages/endpoint_hosts/view/index.test.tsx | 21 +++ .../pages/endpoint_hosts/view/translations.ts | 14 ++ .../endpoint/routes/actions/audit_log.test.ts | 46 +++++++ .../routes/actions/audit_log_handler.ts | 12 +- .../server/endpoint/services/actions.ts | 56 ++++++-- 18 files changed, 416 insertions(+), 67 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.test.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts index fd4d89540f0ce9..98cb7729c9440b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts @@ -23,6 +23,8 @@ export const EndpointActionLogRequestSchema = { query: schema.object({ page: schema.number({ defaultValue: 1, min: 1 }), page_size: schema.number({ defaultValue: 10, min: 1, max: 100 }), + start_date: schema.maybe(schema.string()), + end_date: schema.maybe(schema.string()), }), params: schema.object({ agent_id: schema.string(), diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index dfaad68e295ebd..25fc831ca0aa45 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -60,6 +60,8 @@ export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse; export interface ActivityLog { page: number; pageSize: number; + startDate?: string; + endDate?: string; data: ActivityLogEntry[]; } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts index 42c16e151c45d5..3fe6821abbcbe4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts @@ -146,13 +146,6 @@ export type EndpointIsolationRequestStateChange = Action<'endpointIsolationReque payload: EndpointState['isolationRequestState']; }; -export interface AppRequestedEndpointActivityLog { - type: 'appRequestedEndpointActivityLog'; - payload: { - page: number; - pageSize: number; - }; -} export type EndpointDetailsActivityLogChanged = Action<'endpointDetailsActivityLogChanged'> & { payload: EndpointState['endpointDetails']['activityLog']['logData']; }; @@ -165,9 +158,18 @@ export interface EndpointDetailsActivityLogUpdatePaging { type: 'endpointDetailsActivityLogUpdatePaging'; payload: { // disable paging when no more data after paging - disabled: boolean; + disabled?: boolean; page: number; pageSize: number; + startDate?: string; + endDate?: string; + }; +} + +export interface EndpointDetailsActivityLogUpdateIsInvalidDateRange { + type: 'endpointDetailsActivityLogUpdateIsInvalidDateRange'; + payload: { + isInvalidDateRange?: boolean; }; } @@ -181,8 +183,8 @@ export type EndpointAction = | ServerFailedToReturnEndpointList | ServerReturnedEndpointDetails | ServerFailedToReturnEndpointDetails - | AppRequestedEndpointActivityLog | EndpointDetailsActivityLogUpdatePaging + | EndpointDetailsActivityLogUpdateIsInvalidDateRange | EndpointDetailsFlyoutTabChanged | EndpointDetailsActivityLogChanged | ServerReturnedEndpointPolicyResponse diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts index 5db861d18cd693..2a869095cac811 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts @@ -25,6 +25,9 @@ export const initialEndpointPageState = (): Immutable => { disabled: false, page: 1, pageSize: 50, + startDate: undefined, + endDate: undefined, + isInvalidDateRange: false, }, logData: createUninitialisedResourceState(), }, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index 3bf625d726e5f3..a9c65c74015c6a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -48,6 +48,7 @@ describe('EndpointList store concerns', () => { disabled: false, page: 1, pageSize: 50, + isInvalidDateRange: false, }, logData: { type: 'UninitialisedResourceState' }, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index e34e9cf5a83f38..f233fbdec5415e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -65,6 +65,7 @@ import { resolvePathVariables } from '../../../../common/utils/resolve_path_vari import { EndpointPackageInfoStateChanged } from './action'; import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions'; import { EndpointDetailsTabsTypes } from '../view/details/components/endpoint_details_tabs'; +import { getIsInvalidDateRange } from '../utils'; type EndpointPageStore = ImmutableMiddlewareAPI; @@ -400,21 +401,50 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory(getActivityLogData(getState())), - }); - + if ( + action.type === 'endpointDetailsActivityLogUpdatePaging' && + hasSelectedEndpoint(getState()) + ) { try { - const { page, pageSize } = getActivityLogDataPaging(getState()); + const { disabled, page, pageSize, startDate, endDate } = getActivityLogDataPaging( + getState() + ); + // don't page when paging is disabled or when date ranges are invalid + if (disabled) { + return; + } + if (getIsInvalidDateRange({ startDate, endDate })) { + dispatch({ + type: 'endpointDetailsActivityLogUpdateIsInvalidDateRange', + payload: { + isInvalidDateRange: true, + }, + }); + return; + } + + dispatch({ + type: 'endpointDetailsActivityLogUpdateIsInvalidDateRange', + payload: { + isInvalidDateRange: false, + }, + }); + dispatch({ + type: 'endpointDetailsActivityLogChanged', + // ts error to be fixed when AsyncResourceState is refactored (#830) + // @ts-expect-error + payload: createLoadingResourceState(getActivityLogData(getState())), + }); const route = resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, { agent_id: selectedAgent(getState()), }); const activityLog = await coreStart.http.get(route, { - query: { page, page_size: pageSize }, + query: { + page, + page_size: pageSize, + start_date: startDate, + end_date: endDate, + }, }); const lastLoadedLogData = getLastLoadedActivityLogData(getState()); @@ -428,6 +458,8 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory 1 ? activityLog.page - 1 : 1, pageSize: activityLog.pageSize, + startDate: activityLog.startDate, + endDate: activityLog.endDate, }, }); } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 0981d621f26f32..1498ce08db8abc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -41,6 +41,8 @@ const handleEndpointDetailsActivityLogChanged: CaseReducer; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.test.ts new file mode 100644 index 00000000000000..fa2aaaa16ae376 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { getIsInvalidDateRange } from './utils'; + +describe('utils', () => { + describe('getIsInvalidDateRange', () => { + it('should return FALSE when either dates are undefined', () => { + expect(getIsInvalidDateRange({})).toBe(false); + expect(getIsInvalidDateRange({ startDate: moment().subtract(1, 'd').toISOString() })).toBe( + false + ); + expect(getIsInvalidDateRange({ endDate: moment().toISOString() })).toBe(false); + }); + + it('should return TRUE when startDate is after endDate', () => { + expect( + getIsInvalidDateRange({ + startDate: moment().toISOString(), + endDate: moment().subtract(1, 'd').toISOString(), + }) + ).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts index 3e17992dd975f8..e2d619743c83b2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts @@ -5,6 +5,7 @@ * 2.0. */ +import moment from 'moment'; import { HostInfo, HostMetadata } from '../../../../common/endpoint/types'; export const isPolicyOutOfDate = ( @@ -23,3 +24,18 @@ export const isPolicyOutOfDate = ( reported.endpoint_policy_version >= current.endpoint.revision ); }; + +export const getIsInvalidDateRange = ({ + startDate, + endDate, +}: { + startDate?: string; + endDate?: string; +}) => { + if (startDate && endDate) { + const start = moment(startDate); + const end = moment(endDate); + return start.isAfter(end); + } + return false; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx new file mode 100644 index 00000000000000..f11d2872e3d262 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useDispatch } from 'react-redux'; +import React, { memo, useCallback } from 'react'; +import styled from 'styled-components'; +import moment from 'moment'; +import { EuiFlexGroup, EuiFlexItem, EuiDatePicker, EuiDatePickerRange } from '@elastic/eui'; + +import * as i18 from '../../../translations'; +import { useEndpointSelector } from '../../../hooks'; +import { getActivityLogDataPaging } from '../../../../store/selectors'; + +const DatePickerWrapper = styled.div` + width: ${(props) => props.theme.eui.fractions.single.percentage}; + background: white; +`; +const StickyFlexItem = styled(EuiFlexItem)` + position: sticky; + top: ${(props) => props.theme.eui.euiSizeM}; + z-index: 1; +`; + +export const DateRangePicker = memo(() => { + const dispatch = useDispatch(); + const { page, pageSize, startDate, endDate, isInvalidDateRange } = useEndpointSelector( + getActivityLogDataPaging + ); + + const onClear = useCallback( + ({ clearStart = false, clearEnd = false }: { clearStart?: boolean; clearEnd?: boolean }) => { + dispatch({ + type: 'endpointDetailsActivityLogUpdatePaging', + payload: { + disabled: false, + page, + pageSize, + startDate: clearStart ? undefined : startDate, + endDate: clearEnd ? undefined : endDate, + }, + }); + }, + [dispatch, endDate, startDate, page, pageSize] + ); + + const onChangeStartDate = useCallback( + (date) => { + dispatch({ + type: 'endpointDetailsActivityLogUpdatePaging', + payload: { + disabled: false, + page, + pageSize, + startDate: date ? date?.toISOString() : undefined, + endDate: endDate ? endDate : undefined, + }, + }); + }, + [dispatch, endDate, page, pageSize] + ); + + const onChangeEndDate = useCallback( + (date) => { + dispatch({ + type: 'endpointDetailsActivityLogUpdatePaging', + payload: { + disabled: false, + page, + pageSize, + startDate: startDate ? startDate : undefined, + endDate: date ? date.toISOString() : undefined, + }, + }); + }, + [dispatch, startDate, page, pageSize] + ); + + return ( + + + + + onClear({ clearStart: true })} + placeholderText={i18.ACTIVITY_LOG.datePicker.startDate} + selected={startDate ? moment(startDate) : undefined} + showTimeSelect + startDate={startDate ? moment(startDate) : undefined} + /> + } + endDateControl={ + onClear({ clearEnd: true })} + placeholderText={i18.ACTIVITY_LOG.datePicker.endDate} + selected={endDate ? moment(endDate) : undefined} + showTimeSelect + startDate={startDate ? moment(startDate) : undefined} + /> + } + /> + + + + + ); +}); + +DateRangePicker.displayName = 'DateRangePicker'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx index aa1f56529657ec..73a3734e4ca88a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx @@ -56,19 +56,14 @@ export const EndpointDetailsFlyoutTabs = memo( }, }); if (tab.id === EndpointDetailsTabsTypes.activityLog) { - const paging = { - page: 1, - pageSize, - }; - dispatch({ - type: 'appRequestedEndpointActivityLog', - payload: paging, - }); dispatch({ type: 'endpointDetailsActivityLogUpdatePaging', payload: { disabled: false, - ...paging, + page: 1, + pageSize, + startDate: undefined, + endDate: undefined, }, }); } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx index 360d6e38428161..121f23fdb3a9e7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useCallback, useEffect, useRef } from 'react'; +import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'; import styled from 'styled-components'; import { @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import { LogEntry } from './components/log_entry'; +import { DateRangePicker } from './components/activity_log_date_range_picker'; import * as i18 from '../translations'; import { Immutable, ActivityLog } from '../../../../../../common/endpoint/types'; import { AsyncResourceState } from '../../../../state'; @@ -31,12 +32,12 @@ import { getActivityLogRequestLoading, } from '../../store/selectors'; -const StyledEuiFlexGroup = styled(EuiFlexGroup)` - height: 85vh; +const StyledEuiFlexGroup = styled(EuiFlexGroup)<{ isShorter: boolean }>` + height: ${({ isShorter }) => (isShorter ? '25vh' : '85vh')}; `; const LoadMoreTrigger = styled.div` - height: 6px; - width: 100%; + height: ${(props) => props.theme.eui.euiSizeXS}; + width: ${(props) => props.theme.eui.fractions.single.percentage}; `; export const EndpointActivityLog = memo( @@ -48,25 +49,37 @@ export const EndpointActivityLog = memo( const activityLogSize = activityLogData.length; const activityLogError = useEndpointSelector(getActivityLogError); const dispatch = useDispatch<(action: EndpointAction) => void>(); - const { page, pageSize, disabled: isPagingDisabled } = useEndpointSelector( + const { page, pageSize, startDate, endDate, disabled: isPagingDisabled } = useEndpointSelector( getActivityLogDataPaging ); + const hasActiveDateRange = useMemo(() => !!startDate || !!endDate, [startDate, endDate]); + const showEmptyState = useMemo( + () => (activityLogLoaded && !activityLogSize && !hasActiveDateRange) || activityLogError, + [activityLogLoaded, activityLogSize, hasActiveDateRange, activityLogError] + ); + const isShorter = useMemo( + () => !!(hasActiveDateRange && isPagingDisabled && !activityLogLoading && !activityLogSize), + [hasActiveDateRange, isPagingDisabled, activityLogLoading, activityLogSize] + ); + const loadMoreTrigger = useRef(null); const getActivityLog = useCallback( (entries: IntersectionObserverEntry[]) => { const isTargetIntersecting = entries.some((entry) => entry.isIntersecting); if (isTargetIntersecting && activityLogLoaded && !isPagingDisabled) { dispatch({ - type: 'appRequestedEndpointActivityLog', + type: 'endpointDetailsActivityLogUpdatePaging', payload: { page: page + 1, pageSize, + startDate, + endDate, }, }); } }, - [activityLogLoaded, dispatch, isPagingDisabled, page, pageSize] + [activityLogLoaded, dispatch, isPagingDisabled, page, pageSize, startDate, endDate] ); useEffect(() => { @@ -82,8 +95,8 @@ export const EndpointActivityLog = memo( return ( <> - - {(activityLogLoaded && !activityLogSize) || activityLogError ? ( + + {showEmptyState ? ( ) : ( <> + {activityLogLoaded && activityLogData.map((logEntry) => ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index ee5ef52d00f184..aafac38accd89f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -889,6 +889,27 @@ describe('when on the endpoint list page', () => { const emptyState = await renderResult.queryByTestId('activityLogEmpty'); expect(emptyState).not.toBe(null); }); + + it('should not display empty state with no log data while date range filter is active', async () => { + const activityLogTab = await renderResult.findByTestId('activity_log'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(activityLogTab); + }); + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged('success', { + page: 1, + pageSize: 50, + startDate: new Date().toISOString(), + data: [], + }); + }); + + const emptyState = await renderResult.queryByTestId('activityLogEmpty'); + const dateRangePicker = await renderResult.queryByTestId('activityLogDateRangePicker'); + expect(emptyState).toBe(null); + expect(dateRangePicker).not.toBe(null); + }); }); describe('when showing host Policy Response panel', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts index 89ffd2d23807ef..7759935aa840af 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts @@ -15,6 +15,20 @@ export const ACTIVITY_LOG = { tabTitle: i18n.translate('xpack.securitySolution.endpointDetails.activityLog', { defaultMessage: 'Activity Log', }), + datePicker: { + startDate: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.datePicker.startDate', + { + defaultMessage: 'Pick a start date', + } + ), + endDate: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.datePicker.endDate', + { + defaultMessage: 'Pick an end date', + } + ), + }, LogEntry: { endOfLog: i18n.translate( 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.endOfLog', diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts index c7f07151f87246..d9069444a10d7d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts @@ -60,6 +60,37 @@ describe('Action Log API', () => { }).not.toThrow(); }); + it('should work with all query params', () => { + expect(() => { + EndpointActionLogRequestSchema.query.validate({ + page: 10, + page_size: 100, + start_date: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday + end_date: new Date().toISOString(), // today + }); + }).not.toThrow(); + }); + + it('should work with just startDate', () => { + expect(() => { + EndpointActionLogRequestSchema.query.validate({ + page: 1, + page_size: 100, + start_date: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday + }); + }).not.toThrow(); + }); + + it('should work with just endDate', () => { + expect(() => { + EndpointActionLogRequestSchema.query.validate({ + page: 1, + page_size: 100, + end_date: new Date().toISOString(), // today + }); + }).not.toThrow(); + }); + it('should not work without allowed page and page_size params', () => { expect(() => { EndpointActionLogRequestSchema.query.validate({ page_size: 101 }); @@ -176,5 +207,20 @@ describe('Action Log API', () => { expect(error.message).toEqual(`Error fetching actions log for agent_id ${mockID}`); } }); + + it('should return date ranges if present in the query', async () => { + havingActionsAndResponses([], []); + const startDate = new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(); + const endDate = new Date().toISOString(); + const response = await getActivityLog({ + page: 1, + page_size: 50, + start_date: startDate, + end_date: endDate, + }); + expect(response.ok).toBeCalled(); + expect((response.ok.mock.calls[0][0]?.body as ActivityLog).startDate).toEqual(startDate); + expect((response.ok.mock.calls[0][0]?.body as ActivityLog).endDate).toEqual(endDate); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts index 5e9594f478b311..716c1ab8335594 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts @@ -27,10 +27,18 @@ export const actionsLogRequestHandler = ( return async (context, req, res) => { const { params: { agent_id: elasticAgentId }, - query: { page, page_size: pageSize }, + query: { page, page_size: pageSize, start_date: startDate, end_date: endDate }, } = req; - const body = await getAuditLogResponse({ elasticAgentId, page, pageSize, context, logger }); + const body = await getAuditLogResponse({ + elasticAgentId, + page, + pageSize, + startDate, + endDate, + context, + logger, + }); return res.ok({ body, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts index 9d8db5b9a21545..89f088e322ffab 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts @@ -19,28 +19,37 @@ export const getAuditLogResponse = async ({ elasticAgentId, page, pageSize, + startDate, + endDate, context, logger, }: { elasticAgentId: string; page: number; pageSize: number; + startDate?: string; + endDate?: string; context: SecuritySolutionRequestHandlerContext; logger: Logger; -}): Promise<{ - page: number; - pageSize: number; - data: ActivityLog['data']; -}> => { +}): Promise => { const size = Math.floor(pageSize / 2); const from = page <= 1 ? 0 : page * size - size + 1; const esClient = context.core.elasticsearch.client.asCurrentUser; - - const data = await getActivityLog({ esClient, from, size, elasticAgentId, logger }); + const data = await getActivityLog({ + esClient, + from, + size, + startDate, + endDate, + elasticAgentId, + logger, + }); return { page, pageSize, + startDate, + endDate, data, }; }; @@ -49,6 +58,8 @@ const getActivityLog = async ({ esClient, size, from, + startDate, + endDate, elasticAgentId, logger, }: { @@ -56,6 +67,8 @@ const getActivityLog = async ({ elasticAgentId: string; size: number; from: number; + startDate?: string; + endDate?: string; logger: Logger; }) => { const options = { @@ -67,8 +80,22 @@ const getActivityLog = async ({ let actionsResult; let responsesResult; + const dateFilters = []; + if (startDate) { + dateFilters.push({ range: { '@timestamp': { gte: startDate } } }); + } + if (endDate) { + dateFilters.push({ range: { '@timestamp': { lte: endDate } } }); + } try { + // fetch actions with matching agent_id + const baseActionFilters = [ + { term: { agents: elasticAgentId } }, + { term: { input_type: 'endpoint' } }, + { term: { type: 'INPUT_ACTION' } }, + ]; + const actionsFilters = [...baseActionFilters, ...dateFilters]; actionsResult = await esClient.search( { index: AGENT_ACTIONS_INDEX, @@ -77,11 +104,8 @@ const getActivityLog = async ({ body: { query: { bool: { - filter: [ - { term: { agents: elasticAgentId } }, - { term: { input_type: 'endpoint' } }, - { term: { type: 'INPUT_ACTION' } }, - ], + // @ts-ignore + filter: actionsFilters, }, }, sort: [ @@ -99,6 +123,12 @@ const getActivityLog = async ({ (e) => (e._source as EndpointAction).action_id ); + // fetch responses with matching `action_id`s + const baseResponsesFilter = [ + { term: { agent_id: elasticAgentId } }, + { terms: { action_id: actionIds } }, + ]; + const responsesFilters = [...baseResponsesFilter, ...dateFilters]; responsesResult = await esClient.search( { index: AGENT_ACTIONS_RESULTS_INDEX, @@ -106,7 +136,7 @@ const getActivityLog = async ({ body: { query: { bool: { - filter: [{ term: { agent_id: elasticAgentId } }, { terms: { action_id: actionIds } }], + filter: responsesFilters, }, }, }, From 357264db09fd9c6ce1e5440a52837cd0da798220 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Fri, 9 Jul 2021 09:34:27 -0700 Subject: [PATCH 50/77] [DOCS] Fixes formatting in search sessions doc (#105077) --- docs/discover/search-sessions.asciidoc | 31 ++++++++++++++++++-------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/docs/discover/search-sessions.asciidoc b/docs/discover/search-sessions.asciidoc index b503e8cfba3b40..652583db785ad8 100644 --- a/docs/discover/search-sessions.asciidoc +++ b/docs/discover/search-sessions.asciidoc @@ -72,15 +72,28 @@ behaves differently: [float] ==== Limitations -Certain visualization features do not fully support background search sessions yet. If a dashboard using these features gets restored, -all panels using unsupported features won't load immediately, but instead send out additional data requests which can take a while to complete. -In this case a warning *Your search session is still running* will be shown. +Certain visualization features do not fully support background search sessions. If a dashboard +using these features is restored, +all panels using unsupported features won't load immediately, but instead send out additional +data requests, which can take a while to complete. +The warning *Your search session is still running* is shown. -You can either wait for these additional requests to complete or come back to the dashboard later when all data requests have been finished. +You can either wait for these additional requests to complete or come back to the dashboard later +when all data requests have finished. A panel on a dashboard can behave like this if one of the following features is used: -* *Lens* - A *top values* dimension with an enabled setting *Group other values as "Other"* (configurable in the *Advanced* section of the dimension) -* *Lens* - An *intervals* dimension is used -* *Aggregation based* visualizations - A *terms* aggregation is used with an enabled setting *Group other values in separate bucket* -* *Aggregation based* visualizations - A *histogram* aggregation is used -* *Maps* - Layers using joins, blended layers or tracks layers are used + +**Lens** + +* A *top values* dimension with an enabled *Group other values as "Other"* setting. +This is configurable in the *Advanced* section of the dimension. +* An *intervals* dimension. + +**Aggregation based** visualizations + +* A *terms* aggregation with an enabled *Group other values in separate bucket* setting. +* A *histogram* aggregation. + +**Maps** + +* Layers using joins, blended layers, or tracks layers. From 0809d5d15fab7a3013b89f4e8cdb571385aa1ee6 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 9 Jul 2021 18:53:20 +0200 Subject: [PATCH 51/77] [ML] Add integration tests for `trained_models` API (#104819) * [ML] api integration tests for get trained models endpoint * [ML] delete ingest pipelines after tests execution * [ML] deleteIngestPipeline method * [ML] test for unauthorized user * [ML] tests for model stats * [ML] delete trained model tests * [ML] fix typo * [ML] fix expect package path * [ML] get model pipelines tests * [ML] test for aliases * [ML] add tests for a 404 response * [ML] fix typo * [ML] fix typo --- x-pack/test/api_integration/apis/ml/index.ts | 1 + .../apis/ml/trained_models/delete_model.ts | 67 ++++++++++++++ .../ml/trained_models/get_model_pipelines.ts | 51 ++++++++++ .../apis/ml/trained_models/get_model_stats.ts | 54 +++++++++++ .../apis/ml/trained_models/get_models.ts | 88 ++++++++++++++++++ .../apis/ml/trained_models/index.ts | 17 ++++ x-pack/test/functional/services/ml/api.ts | 92 +++++++++++++++++++ .../functional/services/ml/trained_models.ts | 36 +------- 8 files changed, 371 insertions(+), 35 deletions(-) create mode 100644 x-pack/test/api_integration/apis/ml/trained_models/delete_model.ts create mode 100644 x-pack/test/api_integration/apis/ml/trained_models/get_model_pipelines.ts create mode 100644 x-pack/test/api_integration/apis/ml/trained_models/get_model_stats.ts create mode 100644 x-pack/test/api_integration/apis/ml/trained_models/get_models.ts create mode 100644 x-pack/test/api_integration/apis/ml/trained_models/index.ts diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 7154debc3e195f..394672ac07fc52 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -82,5 +82,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./results')); loadTestFile(require.resolve('./saved_objects')); loadTestFile(require.resolve('./system')); + loadTestFile(require.resolve('./trained_models')); }); } diff --git a/x-pack/test/api_integration/apis/ml/trained_models/delete_model.ts b/x-pack/test/api_integration/apis/ml/trained_models/delete_model.ts new file mode 100644 index 00000000000000..3848330a95fb92 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/trained_models/delete_model.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('DELETE trained_models', () => { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.api.createdTestTrainedModels('regression', 2); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('deletes trained model by id', async () => { + const { body: deleteResponseBody } = await supertest + .delete(`/api/ml/trained_models/dfa_regression_model_n_0`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(deleteResponseBody).to.eql({ acknowledged: true }); + + // verify that model is actually deleted + await supertest + .get(`/api/ml/trained_models/dfa_regression_model_n_0`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + }); + + it('returns 404 if requested trained model does not exist', async () => { + await supertest + .delete(`/api/ml/trained_models/not_existing_model`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + }); + + it('does not allow to delete trained model if the user does not have required permissions', async () => { + await supertest + .delete(`/api/ml/trained_models/dfa_regression_model_n_1`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(403); + + // verify that model has not been deleted + await supertest + .get(`/api/ml/trained_models/dfa_regression_model_n_1`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/trained_models/get_model_pipelines.ts b/x-pack/test/api_integration/apis/ml/trained_models/get_model_pipelines.ts new file mode 100644 index 00000000000000..cc347056f02a31 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/trained_models/get_model_pipelines.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('GET trained_models/pipelines', () => { + let testModelIds: string[] = []; + + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + testModelIds = await ml.api.createdTestTrainedModels('regression', 2, true); + }); + + after(async () => { + // delete all created ingest pipelines + await Promise.all(testModelIds.map((modelId) => ml.api.deleteIngestPipeline(modelId))); + await ml.api.cleanMlIndices(); + }); + + it('returns trained model pipelines by id', async () => { + const { body } = await supertest + .get(`/api/ml/trained_models/dfa_regression_model_n_0/pipelines`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.length).to.eql(1); + expect(body[0].model_id).to.eql('dfa_regression_model_n_0'); + expect(Object.keys(body[0].pipelines).length).to.eql(1); + }); + + it('returns an error in case user does not have required permission', async () => { + await supertest + .get(`/api/ml/trained_models/dfa_regression_model_n_0/pipelines`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(403); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/trained_models/get_model_stats.ts b/x-pack/test/api_integration/apis/ml/trained_models/get_model_stats.ts new file mode 100644 index 00000000000000..76f108836996f2 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/trained_models/get_model_stats.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('GET trained_models/_stats', () => { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.api.createdTestTrainedModels('regression', 2); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('returns trained model stats by id', async () => { + const { body } = await supertest + .get(`/api/ml/trained_models/dfa_regression_model_n_0/_stats`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.count).to.eql(1); + expect(body.trained_model_stats[0].model_id).to.eql('dfa_regression_model_n_0'); + }); + + it('returns 404 if requested trained model does not exist', async () => { + await supertest + .get(`/api/ml/trained_models/not_existing_model/_stats`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + }); + + it('returns an error for unauthorized user', async () => { + await supertest + .get(`/api/ml/trained_models/dfa_regression_model_n_0/_stats`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(403); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/trained_models/get_models.ts b/x-pack/test/api_integration/apis/ml/trained_models/get_models.ts new file mode 100644 index 00000000000000..604dff6a98a9af --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/trained_models/get_models.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('GET trained_models', () => { + let testModelIds: string[] = []; + + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + testModelIds = await ml.api.createdTestTrainedModels('regression', 5, true); + await ml.api.createModelAlias('dfa_regression_model_n_0', 'dfa_regression_model_alias'); + await ml.api.createIngestPipeline('dfa_regression_model_alias'); + }); + + after(async () => { + // delete created ingest pipelines + await Promise.all( + ['dfa_regression_model_alias', ...testModelIds].map((modelId) => + ml.api.deleteIngestPipeline(modelId) + ) + ); + await ml.api.cleanMlIndices(); + }); + + it('returns all trained models with associated pipelines including aliases', async () => { + const { body } = await supertest + .get(`/api/ml/trained_models?with_pipelines=true`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + // Created models + system model + expect(body.length).to.eql(6); + + const sampleModel = body.find((v: any) => v.model_id === 'dfa_regression_model_n_0'); + expect(Object.keys(sampleModel.pipelines).length).to.eql(2); + }); + + it('returns models without pipeline in case user does not have required permission', async () => { + const { body } = await supertest + .get(`/api/ml/trained_models?with_pipelines=true`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + // Created models + system model + expect(body.length).to.eql(6); + const sampleModel = body.find((v: any) => v.model_id === 'dfa_regression_model_n_0'); + expect(sampleModel.pipelines).to.eql(undefined); + }); + + it('returns trained model by id', async () => { + const { body } = await supertest + .get(`/api/ml/trained_models/dfa_regression_model_n_1`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + expect(body.length).to.eql(1); + expect(body[0].model_id).to.eql('dfa_regression_model_n_1'); + }); + + it('returns 404 if requested trained model does not exist', async () => { + await supertest + .get(`/api/ml/trained_models/not_existing_model`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + }); + + it('returns an error for unauthorized user', async () => { + await supertest + .get(`/api/ml/trained_models/dfa_regression_model_n_1`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(403); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/trained_models/index.ts b/x-pack/test/api_integration/apis/ml/trained_models/index.ts new file mode 100644 index 00000000000000..d1812dc188b00e --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/trained_models/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('trained models', function () { + loadTestFile(require.resolve('./get_models')); + loadTestFile(require.resolve('./get_model_stats')); + loadTestFile(require.resolve('./get_model_pipelines')); + loadTestFile(require.resolve('./delete_model')); + }); +} diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 728e3ff8fc8e6d..ec5ca4c6611572 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -8,6 +8,8 @@ import { estypes } from '@elastic/elasticsearch'; import expect from '@kbn/expect'; import { ProvidedType } from '@kbn/test'; +import fs from 'fs'; +import path from 'path'; import { Calendar } from '../../../../plugins/ml/server/models/calendar/index'; import { Annotation } from '../../../../plugins/ml/common/types/annotations'; import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; @@ -25,6 +27,8 @@ import { import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; import { PutTrainedModelConfig } from '../../../../plugins/ml/common/types/trained_models'; +type ModelType = 'regression' | 'classification'; + export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { const es = getService('es'); const log = getService('log'); @@ -943,5 +947,93 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { log.debug('> Trained model crated'); return model; }, + + async createdTestTrainedModels( + modelType: ModelType, + count: number = 10, + withIngestPipelines = false + ) { + const compressedDefinition = this.getCompressedModelDefinition(modelType); + + const modelIds = new Array(count).fill(null).map((v, i) => `dfa_${modelType}_model_n_${i}`); + + const models = modelIds.map((id) => { + return { + model_id: id, + body: { + compressed_definition: compressedDefinition, + inference_config: { + [modelType]: {}, + }, + input: { + field_names: ['common_field'], + }, + } as PutTrainedModelConfig, + }; + }); + + for (const model of models) { + await this.createTrainedModel(model.model_id, model.body); + if (withIngestPipelines) { + await this.createIngestPipeline(model.model_id); + } + } + + return modelIds; + }, + + /** + * Retrieves compressed model definition from the test resources. + * @param modelType + */ + getCompressedModelDefinition(modelType: ModelType) { + return fs.readFileSync( + path.resolve( + __dirname, + 'resources', + 'trained_model_definitions', + `minimum_valid_config_${modelType}.json.gz.b64` + ), + 'utf-8' + ); + }, + + async createModelAlias(modelId: string, modelAlias: string) { + log.debug(`Creating alias for model "${modelId}"`); + await esSupertest + .put(`/_ml/trained_models/${modelId}/model_aliases/${modelAlias}`) + .expect(200); + log.debug('> Model alias created'); + }, + + /** + * Creates ingest pipelines for trained model + * @param modelId + */ + async createIngestPipeline(modelId: string) { + log.debug(`Creating ingest pipeline for trained model with id "${modelId}"`); + const ingestPipeline = await esSupertest + .put(`/_ingest/pipeline/pipeline_${modelId}`) + .send({ + processors: [ + { + inference: { + model_id: modelId, + }, + }, + ], + }) + .expect(200) + .then((res) => res.body); + + log.debug('> Ingest pipeline crated'); + return ingestPipeline; + }, + + async deleteIngestPipeline(modelId: string) { + log.debug(`Deleting ingest pipeline for trained model with id "${modelId}"`); + await esSupertest.delete(`/_ingest/pipeline/pipeline_${modelId}`).expect(200); + log.debug('> Ingest pipeline deleted'); + }, }; } diff --git a/x-pack/test/functional/services/ml/trained_models.ts b/x-pack/test/functional/services/ml/trained_models.ts index ae799efbbd30cb..7a1fa1714ca143 100644 --- a/x-pack/test/functional/services/ml/trained_models.ts +++ b/x-pack/test/functional/services/ml/trained_models.ts @@ -5,12 +5,9 @@ * 2.0. */ -import fs from 'fs'; -import path from 'path'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { MlApi } from './api'; -import { PutTrainedModelConfig } from '../../../../plugins/ml/common/types/trained_models'; import { MlCommonUI } from './common_ui'; type ModelType = 'regression' | 'classification'; @@ -24,38 +21,7 @@ export function TrainedModelsProvider( return { async createdTestTrainedModels(modelType: ModelType, count: number = 10) { - const compressedDefinition = this.getCompressedModelDefinition(modelType); - - const models = new Array(count).fill(null).map((v, i) => { - return { - model_id: `dfa_${modelType}_model_n_${i}`, - body: { - compressed_definition: compressedDefinition, - inference_config: { - [modelType]: {}, - }, - input: { - field_names: ['common_field'], - }, - } as PutTrainedModelConfig, - }; - }); - - for (const model of models) { - await mlApi.createTrainedModel(model.model_id, model.body); - } - }, - - getCompressedModelDefinition(modelType: ModelType) { - return fs.readFileSync( - path.resolve( - __dirname, - 'resources', - 'trained_model_definitions', - `minimum_valid_config_${modelType}.json.gz.b64` - ), - 'utf-8' - ); + await mlApi.createdTestTrainedModels(modelType, count); }, async assertStats(expectedTotalCount: number) { From 95008cdb61b16090166115be8e6e906b4bd47242 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 9 Jul 2021 13:04:52 -0400 Subject: [PATCH 52/77] [Fleet] Fix add host url validation in fleet server setup (#105072) --- .../components/fleet_server_on_prem_instructions.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx index 48ff51f1a25e8c..0fc3821d2e3f7f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx @@ -452,6 +452,8 @@ export const AddFleetServerHostStepContent = ({ await addFleetServerHost(fleetServerHost); setCalloutHost(fleetServerHost); setFleetServerHost(''); + } else { + setCalloutHost(''); } } finally { setIsLoading(false); From 5b207d8484cee16375068791eb4e81b914d9022e Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Fri, 9 Jul 2021 10:18:25 -0700 Subject: [PATCH 53/77] [Reporting] Add `handleSIGHUP: false` to puppeteer LaunchOptions (#104992) --- .../browsers/chromium/driver_factory/index.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 1141437eae0eff..eb2abf4036c03d 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -26,6 +26,18 @@ import { HeadlessChromiumDriver } from '../driver'; import { args } from './args'; import { Metrics, getMetrics } from './metrics'; +// Puppeteer type definitions do not match the documentation. +// See https://pptr.dev/#?product=Puppeteer&version=v8.0.0&show=api-puppeteerlaunchoptions +interface ReportingLaunchOptions extends puppeteer.LaunchOptions { + userDataDir?: string; + ignoreHTTPSErrors?: boolean; + args?: string[]; +} + +declare module 'puppeteer' { + function launch(options: ReportingLaunchOptions): Promise; +} + type BrowserConfig = CaptureConfig['browser']['chromium']; type ViewportConfig = CaptureConfig['viewport']; @@ -85,11 +97,12 @@ export class HeadlessChromiumDriverFactory { userDataDir: this.userDataDir, executablePath: this.binaryPath, ignoreHTTPSErrors: true, + handleSIGHUP: false, args: chromiumArgs, env: { TZ: browserTimezone, }, - } as puppeteer.LaunchOptions); + }); page = await browser.newPage(); devTools = await page.target().createCDPSession(); From d2ce8d52235248b03e69531c23a72f376e082263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 9 Jul 2021 13:46:52 -0400 Subject: [PATCH 54/77] [APM] Make fleet plugin dependency optional (#104967) * fixing tutorial when fleet plugin is disabled * addressing PR comments --- x-pack/plugins/apm/kibana.json | 11 +- x-pack/plugins/apm/public/plugin.ts | 33 +- .../config_agent/config_agent.stories.tsx | 7 + .../config_agent/get_policy_options.test.ts | 8 + .../tutorial/config_agent/index.test.tsx | 346 ++++++++++++++---- .../public/tutorial/config_agent/index.tsx | 51 ++- .../tutorial/config_agent/policy_selector.tsx | 10 +- .../tutorial_fleet_instructions/index.tsx | 1 + x-pack/plugins/apm/server/plugin.ts | 33 +- x-pack/plugins/apm/server/routes/fleet.ts | 10 +- .../apm/server/tutorial/envs/on_prem.ts | 19 +- x-pack/plugins/apm/server/tutorial/index.ts | 4 +- 12 files changed, 384 insertions(+), 149 deletions(-) diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index ae4510b10acd44..9f661f13a491ed 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -7,7 +7,6 @@ "data", "embeddable", "features", - "fleet", "infra", "licensing", "observability", @@ -24,11 +23,15 @@ "security", "spaces", "taskManager", - "usageCollection" + "usageCollection", + "fleet" ], "server": true, "ui": true, - "configPath": ["xpack", "apm"], + "configPath": [ + "xpack", + "apm" + ], "requiredBundles": [ "fleet", "home", @@ -38,4 +41,4 @@ "ml", "observability" ] -} +} \ No newline at end of file diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 0cd50095706138..91b045b8db46f9 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -74,7 +74,7 @@ export interface ApmPluginStartDeps { ml?: MlPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; observability: ObservabilityPublicStart; - fleet: FleetStart; + fleet?: FleetStart; } export class ApmPlugin implements Plugin { @@ -311,20 +311,21 @@ export class ApmPlugin implements Plugin { } public start(core: CoreStart, plugins: ApmPluginStartDeps) { const { fleet } = plugins; - - const agentEnrollmentExtensionData = getApmEnrollmentFlyoutData(); - - fleet.registerExtension({ - package: 'apm', - view: 'agent-enrollment-flyout', - title: agentEnrollmentExtensionData.title, - Component: agentEnrollmentExtensionData.Component, - }); - - fleet.registerExtension({ - package: 'apm', - view: 'package-detail-assets', - Component: LazyApmCustomAssetsExtension, - }); + if (fleet) { + const agentEnrollmentExtensionData = getApmEnrollmentFlyoutData(); + + fleet.registerExtension({ + package: 'apm', + view: 'agent-enrollment-flyout', + title: agentEnrollmentExtensionData.title, + Component: agentEnrollmentExtensionData.Component, + }); + + fleet.registerExtension({ + package: 'apm', + view: 'package-detail-assets', + Component: LazyApmCustomAssetsExtension, + }); + } } } diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/config_agent.stories.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/config_agent.stories.tsx index 33f171ab88247a..0d4d3748422ea0 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/config_agent.stories.tsx +++ b/x-pack/plugins/apm/public/tutorial/config_agent/config_agent.stories.tsx @@ -18,6 +18,7 @@ interface Args { onPrem: boolean; hasFleetPoliciesWithApmIntegration: boolean; hasCloudPolicyWithApmIntegration: boolean; + isFleetEnabled: boolean; } const policyElasticAgentOnCloudAgent: APIResponseType['fleetAgents'][0] = { @@ -47,6 +48,7 @@ function Wrapper({ apmAgent, onPrem, hasCloudPolicyWithApmIntegration, + isFleetEnabled, }: Args) { const http = ({ get: () => ({ @@ -56,6 +58,7 @@ function Wrapper({ ? [policyElasticAgentOnCloudAgent] : []), ], + isFleetEnabled, cloudStandaloneSetup: { apmServerUrl: 'cloud_url', secretToken: 'foo', @@ -80,6 +83,7 @@ Integration.args = { onPrem: true, hasFleetPoliciesWithApmIntegration: false, hasCloudPolicyWithApmIntegration: false, + isFleetEnabled: true, }; export default { @@ -113,5 +117,8 @@ export default { hasCloudPolicyWithApmIntegration: { control: { type: 'boolean', options: [true, false] }, }, + isFleetEnabled: { + control: { type: 'boolean', options: [true, false], defaultValue: true }, + }, }, }; diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/get_policy_options.test.ts b/x-pack/plugins/apm/public/tutorial/config_agent/get_policy_options.test.ts index 90c9aab80f6f56..c6dc7265f3d3e6 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/get_policy_options.test.ts +++ b/x-pack/plugins/apm/public/tutorial/config_agent/get_policy_options.test.ts @@ -41,6 +41,7 @@ describe('getPolicyOptions', () => { apmServerUrl: 'cloud_url', secretToken: 'cloud_token', }, + isFleetEnabled: true, }; const options = getPolicyOptions({ isCloudEnabled: true, @@ -65,6 +66,7 @@ describe('getPolicyOptions', () => { apmServerUrl: 'cloud_url', secretToken: 'cloud_token', }, + isFleetEnabled: true, }; const options = getPolicyOptions({ isCloudEnabled: true, @@ -109,6 +111,7 @@ describe('getPolicyOptions', () => { apmServerUrl: 'cloud_url', secretToken: 'cloud_token', }, + isFleetEnabled: true, }; const options = getPolicyOptions({ isCloudEnabled: true, @@ -151,6 +154,7 @@ describe('getPolicyOptions', () => { const data: APIResponseType = { fleetAgents: [], cloudStandaloneSetup: undefined, + isFleetEnabled: true, }; const options = getPolicyOptions({ isCloudEnabled: true, @@ -173,6 +177,7 @@ describe('getPolicyOptions', () => { const data: APIResponseType = { fleetAgents, cloudStandaloneSetup: undefined, + isFleetEnabled: true, }; const options = getPolicyOptions({ isCloudEnabled: true, @@ -213,6 +218,7 @@ describe('getPolicyOptions', () => { const data: APIResponseType = { fleetAgents: [policyElasticAgentOnCloudAgent, ...fleetAgents], cloudStandaloneSetup: undefined, + isFleetEnabled: true, }; const options = getPolicyOptions({ isCloudEnabled: true, @@ -256,6 +262,7 @@ describe('getPolicyOptions', () => { const data: APIResponseType = { fleetAgents: [], cloudStandaloneSetup: undefined, + isFleetEnabled: true, }; const options = getPolicyOptions({ isCloudEnabled: false, @@ -278,6 +285,7 @@ describe('getPolicyOptions', () => { const data: APIResponseType = { fleetAgents, cloudStandaloneSetup: undefined, + isFleetEnabled: true, }; const options = getPolicyOptions({ isCloudEnabled: false, diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/index.test.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/index.test.tsx index 8f8afe58506a69..cb49cee108bd1a 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/index.test.tsx +++ b/x-pack/plugins/apm/public/tutorial/config_agent/index.test.tsx @@ -7,6 +7,10 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { HttpStart } from 'kibana/public'; import React from 'react'; +import { + expectTextsInDocument, + expectTextsNotInDocument, +} from '../../utils/testHelpers'; import TutorialConfigAgent from './'; const policyElasticAgentOnCloudAgent = { @@ -32,68 +36,32 @@ const fleetAgents = [ ]; describe('TutorialConfigAgent', () => { - it('renders loading component while API is being called', () => { - const component = render( - - ); - expect(component.getByTestId('loading')).toBeInTheDocument(); + beforeAll(() => { + // Mocks console.error so it won't polute tests output when testing the api throwing error + jest.spyOn(console, 'error').mockImplementation(() => null); }); - it('updates commands when a different policy is selected', async () => { - const component = render( - - ); - expect( - await screen.findByText('Default Standalone configuration') - ).toBeInTheDocument(); - let commands = component.getByTestId('commands').innerHTML; - expect(commands).not.toEqual(''); - expect(commands).toMatchInlineSnapshot(` - "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ - -Delastic.apm.service_name=my-application \\\\ - -Delastic.apm.server_urls=http://localhost:8200 \\\\ - -Delastic.apm.secret_token= \\\\ - -Delastic.apm.environment=production \\\\ - -Delastic.apm.application_packages=org.example \\\\ - -jar my-application.jar" - `); - fireEvent.click(component.getByTestId('comboBoxToggleListButton')); - fireEvent.click(component.getByText('agent foo')); - commands = component.getByTestId('commands').innerHTML; - expect(commands).not.toEqual(''); - expect(commands).toMatchInlineSnapshot(` - "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ - -Delastic.apm.service_name=my-application \\\\ - -Delastic.apm.server_urls=foo \\\\ - -Delastic.apm.secret_token=foo \\\\ - -Delastic.apm.environment=production \\\\ - -Delastic.apm.application_packages=org.example \\\\ - -jar my-application.jar" - `); + afterAll(() => { + jest.restoreAllMocks(); }); - describe('running on prem', () => { - it('selects defaul standalone by defauls', async () => { + + describe('when fleet plugin is enabled', () => { + it('renders loading component while API is being called', () => { + const component = render( + + ); + expect(component.getByTestId('loading')).toBeInTheDocument(); + }); + it('updates commands when a different policy is selected', async () => { const component = render( { get: jest.fn().mockReturnValue({ cloudStandaloneSetup: undefined, fleetAgents, + isFleetEnabled: true, }), } as unknown) as HttpStart } @@ -112,10 +81,7 @@ describe('TutorialConfigAgent', () => { expect( await screen.findByText('Default Standalone configuration') ).toBeInTheDocument(); - expect( - component.getByTestId('policySelector_onPrem') - ).toBeInTheDocument(); - const commands = component.getByTestId('commands').innerHTML; + let commands = component.getByTestId('commands').innerHTML; expect(commands).not.toEqual(''); expect(commands).toMatchInlineSnapshot(` "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ @@ -126,21 +92,238 @@ describe('TutorialConfigAgent', () => { -Delastic.apm.application_packages=org.example \\\\ -jar my-application.jar" `); + + fireEvent.click(component.getByTestId('comboBoxToggleListButton')); + fireEvent.click(component.getByText('agent foo')); + commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=foo \\\\ + -Delastic.apm.secret_token=foo \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + }); + describe('running on prem', () => { + it('selects defaul standalone by defauls', async () => { + const component = render( + + ); + expect( + await screen.findByText('Default Standalone configuration') + ).toBeInTheDocument(); + expect( + component.getByTestId('policySelector_onPrem') + ).toBeInTheDocument(); + const commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=http://localhost:8200 \\\\ + -Delastic.apm.secret_token= \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + }); + it('shows get started with fleet link when there are no fleet agents', async () => { + const component = render( + + ); + expect( + await screen.findByText('Default Standalone configuration') + ).toBeInTheDocument(); + expect( + component.getByTestId('policySelector_onPrem') + ).toBeInTheDocument(); + const commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=http://localhost:8200 \\\\ + -Delastic.apm.secret_token= \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + expectTextsInDocument(component, ['Get started with fleet']); + }); + }); + describe('running on cloud', () => { + it('selects defaul standalone by defauls', async () => { + const component = render( + + ); + expect( + await screen.findByText('Default Standalone configuration') + ).toBeInTheDocument(); + expect( + component.getByTestId('policySelector_cloud') + ).toBeInTheDocument(); + const commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=cloud_url \\\\ + -Delastic.apm.secret_token=cloud_token \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + }); + it('selects policy elastic agent on cloud when available by default', async () => { + const component = render( + + ); + expect( + await screen.findByText('Elastic Cloud agent policy') + ).toBeInTheDocument(); + expect( + component.getByTestId('policySelector_policy-elastic-agent-on-cloud') + ).toBeInTheDocument(); + const commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=apm_cloud_url \\\\ + -Delastic.apm.secret_token=apm_cloud_token \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + }); + + it('shows default standalone option when api throws an error', async () => { + const component = render( + { + throw new Error('Boom'); + }, + } as unknown) as HttpStart + } + basePath="http://localhost:5601" + isCloudEnabled + /> + ); + expect( + await screen.findByText('Default Standalone configuration') + ).toBeInTheDocument(); + const commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=http://localhost:8200 \\\\ + -Delastic.apm.secret_token= \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + }); }); }); - describe('running on cloud', () => { - it('selects defaul standalone by defauls', async () => { + describe('when fleet plugin is disabled', () => { + it('hides fleet links', async () => { const component = render( + ); + + expectTextsNotInDocument(component, [ + 'Get started with fleet', + 'Manage fleet policies', + ]); + }); + it('shows default standalone on prem', async () => { + const component = render( + { expect( await screen.findByText('Default Standalone configuration') ).toBeInTheDocument(); - expect(component.getByTestId('policySelector_cloud')).toBeInTheDocument(); + expect( + component.getByTestId('policySelector_onPrem') + ).toBeInTheDocument(); const commands = component.getByTestId('commands').innerHTML; expect(commands).not.toEqual(''); expect(commands).toMatchInlineSnapshot(` "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ -Delastic.apm.service_name=my-application \\\\ - -Delastic.apm.server_urls=cloud_url \\\\ - -Delastic.apm.secret_token=cloud_token \\\\ + -Delastic.apm.server_urls=http://localhost:8200 \\\\ + -Delastic.apm.secret_token= \\\\ -Delastic.apm.environment=production \\\\ -Delastic.apm.application_packages=org.example \\\\ -jar my-application.jar" `); }); - it('selects policy elastic agent on cloud when available by default', async () => { + it('shows default standalone on cloud', async () => { const component = render( { apmServerUrl: 'cloud_url', secretToken: 'cloud_token', }, - fleetAgents: [...fleetAgents, policyElasticAgentOnCloudAgent], + fleetAgents: [], + isFleetEnabled: false, }), } as unknown) as HttpStart } @@ -184,18 +370,16 @@ describe('TutorialConfigAgent', () => { /> ); expect( - await screen.findByText('Elastic Cloud agent policy') - ).toBeInTheDocument(); - expect( - component.getByTestId('policySelector_policy-elastic-agent-on-cloud') + await screen.findByText('Default Standalone configuration') ).toBeInTheDocument(); + expect(component.getByTestId('policySelector_cloud')).toBeInTheDocument(); const commands = component.getByTestId('commands').innerHTML; expect(commands).not.toEqual(''); expect(commands).toMatchInlineSnapshot(` "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ -Delastic.apm.service_name=my-application \\\\ - -Delastic.apm.server_urls=apm_cloud_url \\\\ - -Delastic.apm.secret_token=apm_cloud_token \\\\ + -Delastic.apm.server_urls=cloud_url \\\\ + -Delastic.apm.secret_token=cloud_token \\\\ -Delastic.apm.environment=production \\\\ -Delastic.apm.application_packages=org.example \\\\ -jar my-application.jar" diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx index 755c3eca55868a..d38d51f01c67b7 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx +++ b/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx @@ -46,16 +46,43 @@ interface Props { isCloudEnabled: boolean; } +const INITIAL_STATE = { + fleetAgents: [], + cloudStandaloneSetup: undefined, + isFleetEnabled: false, +}; + +function getFleetLink({ + isFleetEnabled, + hasFleetAgents, + basePath, +}: { + isFleetEnabled: boolean; + hasFleetAgents: boolean; + basePath: string; +}) { + if (!isFleetEnabled) { + return; + } + + return hasFleetAgents + ? { + label: MANAGE_FLEET_POLICIES_LABEL, + href: `${basePath}/app/fleet#/policies`, + } + : { + label: GET_STARTED_WITH_FLEET_LABEL, + href: `${basePath}/app/integrations#/detail/apm-0.3.0/overview`, + }; +} + function TutorialConfigAgent({ variantId, http, basePath, isCloudEnabled, }: Props) { - const [data, setData] = useState({ - fleetAgents: [], - cloudStandaloneSetup: undefined, - }); + const [data, setData] = useState(INITIAL_STATE); const [isLoading, setIsLoading] = useState(true); const [selectedOption, setSelectedOption] = useState(); @@ -68,6 +95,7 @@ function TutorialConfigAgent({ setData(response as APIResponseType); } } catch (e) { + setIsLoading(false); console.error('Error while fetching fleet agents.', e); } } @@ -105,15 +133,6 @@ function TutorialConfigAgent({ }); const hasFleetAgents = !!data.fleetAgents.length; - const fleetLink = hasFleetAgents - ? { - label: MANAGE_FLEET_POLICIES_LABEL, - href: `${basePath}/app/fleet#/policies`, - } - : { - label: GET_STARTED_WITH_FLEET_LABEL, - href: `${basePath}/app/integrations#/detail/apm-0.3.0/overview`, - }; return ( <> @@ -125,7 +144,11 @@ function TutorialConfigAgent({ onChange={(newSelectedOption) => setSelectedOption(newSelectedOption) } - fleetLink={fleetLink} + fleetLink={getFleetLink({ + isFleetEnabled: data.isFleetEnabled, + hasFleetAgents, + basePath, + })} /> diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/policy_selector.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/policy_selector.tsx index 3a0c6d70db82be..25ce7042c4c979 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/policy_selector.tsx +++ b/x-pack/plugins/apm/public/tutorial/config_agent/policy_selector.tsx @@ -21,7 +21,7 @@ interface Props { options: PolicyOption[]; selectedOption?: PolicyOption; onChange: (selectedOption?: PolicyOption) => void; - fleetLink: { + fleetLink?: { label: string; href: string; }; @@ -58,9 +58,11 @@ export function PolicySelector({ { defaultMessage: 'Choose policy' } )} labelAppend={ - - {fleetLink.label} - + fleetLink && ( + + {fleetLink.label} + + ) } helpText={i18n.translate( 'xpack.apm.tutorial.agent_config.choosePolicy.helper', diff --git a/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx b/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx index 8a81b7a994e761..6fcf13345538f8 100644 --- a/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx +++ b/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx @@ -42,6 +42,7 @@ function TutorialFleetInstructions({ http, basePath, isDarkTheme }: Props) { const response = await http.get('/api/apm/fleet/has_data'); setData(response as APIResponseType); } catch (e) { + setIsLoading(false); console.error('Error while fetching fleet details.', e); } setIsLoading(false); diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index f260971c3bdcbd..3a7eb738dd3b22 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -15,7 +15,7 @@ import { Plugin, PluginInitializerContext, } from 'src/core/server'; -import { mapValues, once } from 'lodash'; +import { isEmpty, mapValues, once } from 'lodash'; import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../rule_registry/common/assets'; import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map'; import { APMConfig, APMXPackConfig, APM_SERVER_FEATURE_ID } from '.'; @@ -104,21 +104,6 @@ export class APMPlugin }); } - plugins.home?.tutorials.registerTutorial( - tutorialProvider({ - isEnabled: this.currentConfig['xpack.apm.ui.enabled'], - indexPatternTitle: this.currentConfig['apm_oss.indexPattern'], - cloud: plugins.cloud, - indices: { - errorIndices: this.currentConfig['apm_oss.errorIndices'], - metricsIndices: this.currentConfig['apm_oss.metricsIndices'], - onboardingIndices: this.currentConfig['apm_oss.onboardingIndices'], - sourcemapIndices: this.currentConfig['apm_oss.sourcemapIndices'], - transactionIndices: this.currentConfig['apm_oss.transactionIndices'], - }, - }) - ); - plugins.features.registerKibanaFeature(APM_FEATURE); registerFeaturesUsage({ licensingPlugin: plugins.licensing }); @@ -206,6 +191,22 @@ export class APMPlugin }; }) as APMRouteHandlerResources['plugins']; + plugins.home?.tutorials.registerTutorial( + tutorialProvider({ + isEnabled: this.currentConfig['xpack.apm.ui.enabled'], + indexPatternTitle: this.currentConfig['apm_oss.indexPattern'], + cloud: plugins.cloud, + isFleetPluginEnabled: !isEmpty(resourcePlugins.fleet), + indices: { + errorIndices: this.currentConfig['apm_oss.errorIndices'], + metricsIndices: this.currentConfig['apm_oss.metricsIndices'], + onboardingIndices: this.currentConfig['apm_oss.onboardingIndices'], + sourcemapIndices: this.currentConfig['apm_oss.sourcemapIndices'], + transactionIndices: this.currentConfig['apm_oss.transactionIndices'], + }, + }) + ); + const telemetryUsageCounter = resourcePlugins.usageCollection?.setup.createUsageCounter( APM_SERVER_FEATURE_ID ); diff --git a/x-pack/plugins/apm/server/routes/fleet.ts b/x-pack/plugins/apm/server/routes/fleet.ts index b83bfd54b93cd2..6628d29b256f7e 100644 --- a/x-pack/plugins/apm/server/routes/fleet.ts +++ b/x-pack/plugins/apm/server/routes/fleet.ts @@ -32,7 +32,7 @@ const hasFleetDataRoute = createApmServerRoute({ handler: async ({ core, plugins }) => { const fleetPluginStart = await plugins.fleet?.start(); if (!fleetPluginStart) { - throw Boom.internal(FLEET_REQUIRED_MESSAGE); + return { hasData: false }; } const packagePolicies = await getApmPackgePolicies({ core, @@ -56,7 +56,7 @@ const fleetAgentsRoute = createApmServerRoute({ const fleetPluginStart = await plugins.fleet?.start(); if (!fleetPluginStart) { - throw Boom.internal(FLEET_REQUIRED_MESSAGE); + return { cloudStandaloneSetup, fleetAgents: [], isFleetEnabled: false }; } // fetches package policies that contains APM integrations const packagePolicies = await getApmPackgePolicies({ @@ -75,6 +75,7 @@ const fleetAgentsRoute = createApmServerRoute({ return { cloudStandaloneSetup, + isFleetEnabled: true, fleetAgents: fleetAgents.map((agent) => { const packagePolicy = policiesGroupedById[agent.id]; const packagePolicyVars = packagePolicy.inputs[0]?.vars; @@ -190,11 +191,6 @@ export const apmFleetRouteRepository = createApmServerRouteRepository() .add(getMigrationCheckRoute) .add(createCloudApmPackagePolicyRoute); -const FLEET_REQUIRED_MESSAGE = i18n.translate( - 'xpack.apm.fleet_has_data.fleetRequired', - { defaultMessage: `Fleet plugin is required` } -); - const FLEET_SECURITY_REQUIRED_MESSAGE = i18n.translate( 'xpack.apm.api.fleet.fleetSecurityRequired', { defaultMessage: `Fleet and Security plugins are required` } diff --git a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts index 882d45c4c21db0..400da79e3d2d06 100644 --- a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts +++ b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts @@ -38,12 +38,14 @@ export function onPremInstructions({ metricsIndices, sourcemapIndices, onboardingIndices, + isFleetPluginEnabled, }: { errorIndices: string; transactionIndices: string; metricsIndices: string; sourcemapIndices: string; onboardingIndices: string; + isFleetPluginEnabled: boolean; }): InstructionsSchema { const EDIT_CONFIG = createEditConfig(); const START_SERVER_UNIX = createStartServerUnix(); @@ -69,12 +71,17 @@ export function onPremInstructions({ iconType: 'alert', }, instructionVariants: [ - { - id: INSTRUCTION_VARIANT.FLEET, - instructions: [ - { customComponentName: 'TutorialFleetInstructions' }, - ], - }, + // hides fleet section when plugin is disabled + ...(isFleetPluginEnabled + ? [ + { + id: INSTRUCTION_VARIANT.FLEET, + instructions: [ + { customComponentName: 'TutorialFleetInstructions' }, + ], + }, + ] + : []), { id: INSTRUCTION_VARIANT.OSX, instructions: [ diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index 9118c30b845d0b..edf056a6d1be4d 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -28,6 +28,7 @@ export const tutorialProvider = ({ indexPatternTitle, indices, cloud, + isFleetPluginEnabled, }: { isEnabled: boolean; indexPatternTitle: string; @@ -39,6 +40,7 @@ export const tutorialProvider = ({ sourcemapIndices: string; onboardingIndices: string; }; + isFleetPluginEnabled: boolean; }) => () => { const savedObjects = [ { @@ -104,7 +106,7 @@ It allows you to monitor the performance of thousands of applications in real ti euiIconType: 'apmApp', artifacts, customStatusCheckName: 'apm_fleet_server_status_check', - onPrem: onPremInstructions(indices), + onPrem: onPremInstructions({ ...indices, isFleetPluginEnabled }), elasticCloud: createElasticCloudInstructions(cloud), previewImagePath: '/plugins/apm/assets/apm.png', savedObjects, From fa03028688aa145bb1483ed5be812e3d7a139353 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Fri, 9 Jul 2021 13:42:50 -0500 Subject: [PATCH 55/77] Enable CSS-in-JS styling with `emotion` (#98157) * emotion deps * kbn-babel * kbn-test * examples * babel-plugin-styled-components config * css prop type fixes * type context * declaration location * some emotion types resolved * clean up * emotion v10 accomodations * types * kbn-crypto * kbn-telemetry-tools * bazel * eslint rule; shared file regex array * update paths * Update packages/kbn-eslint-plugin-eslint/rules/module_migration.js Co-authored-by: Spencer * remove placeholder styles * doc api changes * snapshot updates * storybook comments * use constant * bump new deps * condense versions Co-authored-by: Spencer --- package.json | 3 + .../elastic-eslint-config-kibana/.eslintrc.js | 10 +- packages/kbn-babel-preset/BUILD.bazel | 1 + packages/kbn-babel-preset/webpack_preset.js | 34 +++- packages/kbn-crypto/BUILD.bazel | 3 +- packages/kbn-dev-utils/src/babel.ts | 11 ++ .../rules/module_migration.js | 23 ++- packages/kbn-storybook/lib/default_config.ts | 20 +- packages/kbn-storybook/lib/theme_switcher.tsx | 1 + packages/kbn-telemetry-tools/BUILD.bazel | 3 +- packages/kbn-test/jest-preset.js | 1 + packages/kbn-ui-shared-deps/BUILD.bazel | 1 + packages/kbn-ui-shared-deps/src/entry.js | 1 + packages/kbn-ui-shared-deps/src/index.js | 1 + .../collapsible_nav.test.tsx.snap | 20 +- src/plugins/data/public/public.api.md | 4 +- .../__snapshots__/data_view.test.tsx.snap | 2 - src/plugins/embeddable/public/public.api.md | 5 +- .../tsconfig.json | 3 +- test/tsconfig.json | 2 +- tsconfig.base.json | 8 +- typings/@emotion/index.d.ts | 12 ++ .../report_listing.test.tsx.snap | 27 --- ...screen_capture_panel_content.test.tsx.snap | 12 +- yarn.lock | 187 ++++++++++++++---- 25 files changed, 280 insertions(+), 115 deletions(-) create mode 100644 typings/@emotion/index.d.ts diff --git a/package.json b/package.json index 8f56d80c584ea6..22eedde59c5e7d 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "@elastic/safer-lodash-set": "link:bazel-bin/packages/elastic-safer-lodash-set", "@elastic/search-ui-app-search-connector": "^1.6.0", "@elastic/ui-ace": "0.2.3", + "@emotion/react": "^11.4.0", "@hapi/accept": "^5.0.2", "@hapi/boom": "^9.1.1", "@hapi/cookie": "^11.0.2", @@ -454,6 +455,8 @@ "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/github-checks-reporter": "0.0.20b3", "@elastic/makelogs": "^6.0.0", + "@emotion/babel-preset-css-prop": "^11.2.0", + "@emotion/jest": "^11.3.0", "@istanbuljs/schema": "^0.1.2", "@jest/reporters": "^26.6.2", "@kbn/babel-code-parser": "link:bazel-bin/packages/kbn-babel-code-parser", diff --git a/packages/elastic-eslint-config-kibana/.eslintrc.js b/packages/elastic-eslint-config-kibana/.eslintrc.js index 3220a01184004f..d3cf7cf964a60d 100644 --- a/packages/elastic-eslint-config-kibana/.eslintrc.js +++ b/packages/elastic-eslint-config-kibana/.eslintrc.js @@ -1,3 +1,5 @@ +const { USES_STYLED_COMPONENTS } = require('@kbn/dev-utils'); + module.exports = { extends: [ './javascript.js', @@ -79,7 +81,13 @@ module.exports = { from: 'react-intl', to: '@kbn/i18n/react', disallowedMessage: `import from @kbn/i18n/react instead` - } + }, + { + from: 'styled-components', + to: false, + exclude: USES_STYLED_COMPONENTS, + disallowedMessage: `Prefer using @emotion/react instead. To use styled-components, ensure you plugin is enabled in @kbn/dev-utils/src/babel.ts.` + }, ], ], }, diff --git a/packages/kbn-babel-preset/BUILD.bazel b/packages/kbn-babel-preset/BUILD.bazel index f5ebc153b9e1a0..11eae8bc55ca96 100644 --- a/packages/kbn-babel-preset/BUILD.bazel +++ b/packages/kbn-babel-preset/BUILD.bazel @@ -32,6 +32,7 @@ DEPS = [ "@npm//@babel/preset-env", "@npm//@babel/preset-react", "@npm//@babel/preset-typescript", + "@npm//@emotion/babel-preset-css-prop", "@npm//babel-plugin-add-module-exports", "@npm//babel-plugin-styled-components", "@npm//babel-plugin-transform-react-remove-prop-types", diff --git a/packages/kbn-babel-preset/webpack_preset.js b/packages/kbn-babel-preset/webpack_preset.js index ca7ea40ff0fe1b..186ce87478828c 100644 --- a/packages/kbn-babel-preset/webpack_preset.js +++ b/packages/kbn-babel-preset/webpack_preset.js @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +const { USES_STYLED_COMPONENTS } = require.resolve('@kbn/dev-utils'); + module.exports = () => { return { presets: [ @@ -21,14 +23,6 @@ module.exports = () => { ], require('./common_preset'), ], - plugins: [ - [ - require.resolve('babel-plugin-styled-components'), - { - fileName: false, - }, - ], - ], env: { production: { plugins: [ @@ -42,5 +36,29 @@ module.exports = () => { ], }, }, + overrides: [ + { + include: USES_STYLED_COMPONENTS, + plugins: [ + [ + require.resolve('babel-plugin-styled-components'), + { + fileName: false, + }, + ], + ], + }, + { + exclude: USES_STYLED_COMPONENTS, + presets: [ + [ + require.resolve('@emotion/babel-preset-css-prop'), + { + labelFormat: '[local]', + }, + ], + ], + }, + ], }; }; diff --git a/packages/kbn-crypto/BUILD.bazel b/packages/kbn-crypto/BUILD.bazel index 20793e27de6298..bf1ed3f7789756 100644 --- a/packages/kbn-crypto/BUILD.bazel +++ b/packages/kbn-crypto/BUILD.bazel @@ -38,7 +38,8 @@ TYPES_DEPS = [ "@npm//@types/node", "@npm//@types/node-forge", "@npm//@types/testing-library__jest-dom", - "@npm//resize-observer-polyfill" + "@npm//resize-observer-polyfill", + "@npm//@emotion/react", ] DEPS = SRC_DEPS + TYPES_DEPS diff --git a/packages/kbn-dev-utils/src/babel.ts b/packages/kbn-dev-utils/src/babel.ts index 9daa7d9fe8d7a0..5570055a21d15d 100644 --- a/packages/kbn-dev-utils/src/babel.ts +++ b/packages/kbn-dev-utils/src/babel.ts @@ -46,3 +46,14 @@ export async function transformFileWithBabel(file: File) { file.extname = '.js'; transformedFiles.add(file); } + +/** + * Synchronized regex list of files that use `styled-components`. + * Used by `kbn-babel-preset` and `elastic-eslint-config-kibana`. + */ +export const USES_STYLED_COMPONENTS = [ + /packages[\/\\]kbn-ui-shared-deps[\/\\]/, + /src[\/\\]plugins[\/\\](data|kibana_react)[\/\\]/, + /x-pack[\/\\]plugins[\/\\](apm|beats_management|cases|fleet|infra|lists|observability|osquery|security_solution|timelines|uptime)[\/\\]/, + /x-pack[\/\\]test[\/\\]plugin_functional[\/\\]plugins[\/\\]resolver_test[\/\\]/, +]; diff --git a/packages/kbn-eslint-plugin-eslint/rules/module_migration.js b/packages/kbn-eslint-plugin-eslint/rules/module_migration.js index 87a1bae8eac1af..3175210eccb103 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/module_migration.js +++ b/packages/kbn-eslint-plugin-eslint/rules/module_migration.js @@ -78,6 +78,12 @@ module.exports = { disallowedMessage: { type: 'string', }, + include: { + type: 'array', + }, + exclude: { + type: 'array', + }, }, anyOf: [ { @@ -95,7 +101,22 @@ module.exports = { ], }, create: (context) => { - const mappings = context.options[0]; + const filename = path.relative(KIBANA_ROOT, context.getFilename()); + + const mappings = context.options[0].filter((mapping) => { + // exclude mapping rule if it is explicitly excluded from this file + if (mapping.exclude && mapping.exclude.some((p) => p.test(filename))) { + return false; + } + + // if this mapping rule is only included in specific files, optionally include it + if (mapping.include) { + return mapping.include.some((p) => p.test(filename)); + } + + // include all mapping rules by default + return true; + }); return { ImportDeclaration(node) { diff --git a/packages/kbn-storybook/lib/default_config.ts b/packages/kbn-storybook/lib/default_config.ts index e194c9789daab8..989f707b06fede 100644 --- a/packages/kbn-storybook/lib/default_config.ts +++ b/packages/kbn-storybook/lib/default_config.ts @@ -6,8 +6,11 @@ * Side Public License, v 1. */ +import * as path from 'path'; import { StorybookConfig } from '@storybook/core/types'; +import { REPO_ROOT } from './constants'; +const toPath = (_path: string) => path.join(REPO_ROOT, _path); export const defaultConfig: StorybookConfig = { addons: ['@kbn/storybook/preset', '@storybook/addon-a11y', '@storybook/addon-essentials'], stories: ['../**/*.stories.tsx'], @@ -22,6 +25,21 @@ export const defaultConfig: StorybookConfig = { config.node = { fs: 'empty' }; - return config; + // Remove when @storybook has moved to @emotion v11 + // https://github.com/storybookjs/storybook/issues/13145 + const emotion11CompatibleConfig = { + ...config, + resolve: { + ...config.resolve, + alias: { + ...config.resolve?.alias, + '@emotion/core': toPath('node_modules/@emotion/react'), + '@emotion/styled': toPath('node_modules/@emotion/styled'), + 'emotion-theming': toPath('node_modules/@emotion/react'), + }, + }, + }; + + return emotion11CompatibleConfig; }, }; diff --git a/packages/kbn-storybook/lib/theme_switcher.tsx b/packages/kbn-storybook/lib/theme_switcher.tsx index da62bc7010c4b2..24ddec1fdf51cd 100644 --- a/packages/kbn-storybook/lib/theme_switcher.tsx +++ b/packages/kbn-storybook/lib/theme_switcher.tsx @@ -54,6 +54,7 @@ export function ThemeSwitcher() { closeOnClick tooltip={({ onHide }) => } > + {/* @ts-ignore Remove when @storybook has moved to @emotion v11 */} diff --git a/packages/kbn-telemetry-tools/BUILD.bazel b/packages/kbn-telemetry-tools/BUILD.bazel index d394b0c93d45fb..ef1316cec75a3d 100644 --- a/packages/kbn-telemetry-tools/BUILD.bazel +++ b/packages/kbn-telemetry-tools/BUILD.bazel @@ -47,7 +47,8 @@ TYPES_DEPS = [ "@npm//@types/node", "@npm//@types/normalize-path", "@npm//@types/testing-library__jest-dom", - "@npm//resize-observer-polyfill" + "@npm//resize-observer-polyfill", + "@npm//@emotion/react", ] DEPS = SRC_DEPS + TYPES_DEPS diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index c84fe3f7a55b05..abc5cfa8efaa80 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -66,6 +66,7 @@ module.exports = { snapshotSerializers: [ '/src/plugins/kibana_react/public/util/test_helpers/react_mount_serializer.ts', '/node_modules/enzyme-to-json/serializer', + '/node_modules/@emotion/jest/serializer', ], // The test environment that will be used for testing diff --git a/packages/kbn-ui-shared-deps/BUILD.bazel b/packages/kbn-ui-shared-deps/BUILD.bazel index 9096905a2586be..f92049292f373f 100644 --- a/packages/kbn-ui-shared-deps/BUILD.bazel +++ b/packages/kbn-ui-shared-deps/BUILD.bazel @@ -40,6 +40,7 @@ SRC_DEPS = [ "@npm//@elastic/charts", "@npm//@elastic/eui", "@npm//@elastic/numeral", + "@npm//@emotion/react", "@npm//abortcontroller-polyfill", "@npm//angular", "@npm//babel-loader", diff --git a/packages/kbn-ui-shared-deps/src/entry.js b/packages/kbn-ui-shared-deps/src/entry.js index 0e91c45ae6392a..20e26ca6a28642 100644 --- a/packages/kbn-ui-shared-deps/src/entry.js +++ b/packages/kbn-ui-shared-deps/src/entry.js @@ -18,6 +18,7 @@ export const KbnI18n = require('@kbn/i18n'); export const KbnI18nAngular = require('@kbn/i18n/angular'); export const KbnI18nReact = require('@kbn/i18n/react'); export const Angular = require('angular'); +export const EmotionReact = require('@emotion/react'); export const Moment = require('moment'); export const MomentTimezone = require('moment-timezone/moment-timezone'); export const KbnMonaco = require('@kbn/monaco'); diff --git a/packages/kbn-ui-shared-deps/src/index.js b/packages/kbn-ui-shared-deps/src/index.js index 36c2e6b02879ee..291c7c471d27ce 100644 --- a/packages/kbn-ui-shared-deps/src/index.js +++ b/packages/kbn-ui-shared-deps/src/index.js @@ -57,6 +57,7 @@ exports.externals = { '@kbn/i18n': '__kbnSharedDeps__.KbnI18n', '@kbn/i18n/angular': '__kbnSharedDeps__.KbnI18nAngular', '@kbn/i18n/react': '__kbnSharedDeps__.KbnI18nReact', + '@emotion/react': '__kbnSharedDeps__.EmotionReact', jquery: '__kbnSharedDeps__.Jquery', moment: '__kbnSharedDeps__.Moment', 'moment-timezone': '__kbnSharedDeps__.MomentTimezone', diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 82353a96dc33c6..6e33e39b148c4f 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -746,9 +746,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` onResize={[Function]} >
-
+
@@ -1021,9 +1019,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` onResize={[Function]} >
-
+
@@ -1315,9 +1311,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` onResize={[Function]} >
-
+
@@ -1570,9 +1564,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` onResize={[Function]} >
-
+
@@ -1786,9 +1778,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` onResize={[Function]} >
-
+
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 66d81d058fc77e..b8af7c12d57fcc 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -17,7 +17,6 @@ import { CoreSetup } from 'src/core/public'; import { CoreSetup as CoreSetup_2 } from 'kibana/public'; import { CoreStart } from 'kibana/public'; import { CoreStart as CoreStart_2 } from 'src/core/public'; -import * as CSS from 'csstype'; import { Datatable as Datatable_2 } from 'src/plugins/expressions'; import { Datatable as Datatable_3 } from 'src/plugins/expressions/common'; import { DatatableColumn as DatatableColumn_2 } from 'src/plugins/expressions'; @@ -72,13 +71,12 @@ import { Plugin } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_3 } from 'kibana/public'; import { PopoverAnchorPosition } from '@elastic/eui'; -import * as PropTypes from 'prop-types'; import { PublicContract } from '@kbn/utility-types'; import { PublicMethodsOf } from '@kbn/utility-types'; import { PublicUiSettingsParams } from 'src/core/server/types'; import { RangeFilter as RangeFilter_2 } from 'src/plugins/data/public'; import React from 'react'; -import * as React_3 from 'react'; +import * as React_2 from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; import { Request as Request_2 } from '@hapi/hapi'; import { RequestAdapter } from 'src/plugins/inspector/common'; diff --git a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap index 0ab3f8a4e34668..1e7b59d8a9e766 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap +++ b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap @@ -1169,7 +1169,6 @@ exports[`Inspector Data View component should render single table without select > { - // Warning: (ae-forgotten-export) The symbol "React" needs to be exported by the entry point index.d.ts - constructor(getFactory: EmbeddableStart_2['getEmbeddableFactory'], getAllFactories: EmbeddableStart_2['getEmbeddableFactories'], overlays: OverlayStart_2, notifications: NotificationsStart_2, SavedObjectFinder: React_2.ComponentType, reportUiCounter?: ((appName: string, type: import("@kbn/analytics").UiCounterMetricType, eventNames: string | string[], count?: number | undefined) => void) | undefined); + constructor(getFactory: EmbeddableStart_2['getEmbeddableFactory'], getAllFactories: EmbeddableStart_2['getEmbeddableFactories'], overlays: OverlayStart_2, notifications: NotificationsStart_2, SavedObjectFinder: React.ComponentType, reportUiCounter?: ((appName: string, type: import("@kbn/analytics").UiCounterMetricType, eventNames: string | string[], count?: number | undefined) => void) | undefined); // (undocumented) execute(context: ActionExecutionContext_2): Promise; // (undocumented) diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json index b704274a58aa42..e92dc717ae25ed 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json @@ -6,7 +6,8 @@ "types": [ "node", "jest", - "react" + "react", + "@emotion/react/types/css-prop" ] }, "include": [ diff --git a/test/tsconfig.json b/test/tsconfig.json index 8cf33d93a40674..dccbe8d715c513 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -6,7 +6,7 @@ "emitDeclarationOnly": true, "declaration": true, "declarationMap": true, - "types": ["node", "resize-observer-polyfill"] + "types": ["node", "resize-observer-polyfill", "@emotion/react/types/css-prop"] }, "include": [ "**/*", diff --git a/tsconfig.base.json b/tsconfig.base.json index cc8b66848a394c..0c8fec7c88cda8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -12,7 +12,10 @@ // Allows for importing from `kibana` package for the exported types. "kibana": ["./kibana"], "kibana/public": ["src/core/public"], - "kibana/server": ["src/core/server"] + "kibana/server": ["src/core/server"], + "@emotion/core": [ + "typings/@emotion" + ], }, // Support .tsx files and transform JSX into calls to React.createElement "jsx": "react", @@ -62,7 +65,8 @@ "flot", "jest-styled-components", "@testing-library/jest-dom", - "resize-observer-polyfill" + "resize-observer-polyfill", + "@emotion/react/types/css-prop" ] } } diff --git a/typings/@emotion/index.d.ts b/typings/@emotion/index.d.ts new file mode 100644 index 00000000000000..2a5e63a3e29ef6 --- /dev/null +++ b/typings/@emotion/index.d.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Stub @emotion/core +// Remove when @storybook has moved to @emotion v11 +// https://github.com/storybookjs/storybook/issues/13145 +export {}; diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap index 744a3b2d405c3d..8007acad93e4bd 100644 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap @@ -424,7 +424,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -481,7 +480,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -523,7 +521,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -1491,7 +1487,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -1533,7 +1528,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -2515,7 +2508,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -2557,7 +2549,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -3586,7 +3576,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -3628,7 +3617,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -4690,7 +4677,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -4732,7 +4718,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -5761,7 +5745,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -5803,7 +5786,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -6832,7 +6813,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -6874,7 +6854,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -7903,7 +7881,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -7945,7 +7922,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -8974,7 +8949,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -9016,7 +8990,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
-
+
@@ -752,9 +750,7 @@ exports[`ScreenCapturePanelContent properly renders a view with "print" layout o onResize={[Function]} >
-
+
@@ -1064,9 +1060,7 @@ exports[`ScreenCapturePanelContent renders the default view properly 1`] = ` onResize={[Function]} >
-
+
diff --git a/yarn.lock b/yarn.lock index 227301eed8dd6c..dccbd8f91a4299 100644 --- a/yarn.lock +++ b/yarn.lock @@ -209,6 +209,13 @@ dependencies: "@babel/types" "^7.12.5" +"@babel/helper-module-imports@^7.7.0": + version "7.13.12" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977" + integrity sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA== + dependencies: + "@babel/types" "^7.13.12" + "@babel/helper-module-transforms@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c" @@ -548,6 +555,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-syntax-jsx@^7.2.0": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.13.tgz#044fb81ebad6698fe62c478875575bcbb9b70f15" + integrity sha512-d4HM23Q1K7oq/SLNmG6mRt85l2csmQ0cHRaxRXjKW0YFdEXqlZ5kzFQKH5Uc3rDJECgu+yCRgPkG04Mm98R/1g== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + "@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -1154,6 +1168,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.13.10": + version "7.13.17" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.17.tgz#8966d1fc9593bf848602f0662d6b4d0069e3a7ec" + integrity sha512-NCdgJEelPTSh+FEFylhnP1ylq848l1z9t9N0j1Lfbcw0+KXGjsTvUmkxy+voLLXB5SOKMbLLx4jxYliGrYQseA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.4", "@babel/template@^7.12.13", "@babel/template@^7.12.7", "@babel/template@^7.3.3", "@babel/template@^7.4.4": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" @@ -1187,6 +1208,15 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.13.12": + version "7.13.14" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.14.tgz#c35a4abb15c7cd45a2746d78ab328e362cbace0d" + integrity sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ== + dependencies: + "@babel/helper-validator-identifier" "^7.12.11" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@base2/pretty-print-object@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.0.tgz#860ce718b0b73f4009e153541faff2cb6b85d047" @@ -1592,6 +1622,41 @@ resolved "https://registry.yarnpkg.com/@elastic/ui-ace/-/ui-ace-0.2.3.tgz#5281aed47a79b7216c55542b0675e435692f20cd" integrity sha512-Nti5s2dplBPhSKRwJxG9JXTMOev4jVOWcnTJD1TOkJr1MUBYKVZcNcJtIVMSvahWGmP0B/UfO9q9lyRqdivkvQ== +"@emotion/babel-plugin-jsx-pragmatic@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin-jsx-pragmatic/-/babel-plugin-jsx-pragmatic-0.1.5.tgz#27debfe9c27c4d83574d509787ae553bf8a34d7e" + integrity sha512-y+3AJ0SItMDaAgGPVkQBC/S/BaqaPACkQ6MyCI2CUlrjTxKttTVfD3TMtcs7vLEcLxqzZ1xiG0vzwCXjhopawQ== + dependencies: + "@babel/plugin-syntax-jsx" "^7.2.0" + +"@emotion/babel-plugin@^11.2.0": + version "11.2.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.2.0.tgz#f25c6df8ec045dad5ae6ca63df0791673b98c920" + integrity sha512-lsnQBnl3l4wu/FJoyHnYRpHJeIPNkOBMbtDUIXcO8luulwRKZXPvA10zd2eXVN6dABIWNX4E34en/jkejIg/yA== + dependencies: + "@babel/helper-module-imports" "^7.7.0" + "@babel/plugin-syntax-jsx" "^7.12.1" + "@babel/runtime" "^7.7.2" + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.5" + "@emotion/serialize" "^1.0.0" + babel-plugin-macros "^2.6.1" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "^4.0.3" + +"@emotion/babel-preset-css-prop@^11.2.0": + version "11.2.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-preset-css-prop/-/babel-preset-css-prop-11.2.0.tgz#c7e945f56b2610b438f0dc8ae5253fc55488de0e" + integrity sha512-9XLQm2eLPYTho+Cx1LQTDA1rATjoAaB4O+ds55XDvoAa+Z16Hhg8y5Vihj3C8E6+ilDM8SV5A9Z6z+yj0YIRBg== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.12.1" + "@babel/runtime" "^7.7.2" + "@emotion/babel-plugin" "^11.2.0" + "@emotion/babel-plugin-jsx-pragmatic" "^0.1.5" + "@emotion/babel-utils@^0.6.4": version "0.6.10" resolved "https://registry.yarnpkg.com/@emotion/babel-utils/-/babel-utils-0.6.10.tgz#83dbf3dfa933fae9fc566e54fbb45f14674c6ccc" @@ -1614,6 +1679,17 @@ "@emotion/utils" "0.11.3" "@emotion/weak-memoize" "0.2.5" +"@emotion/cache@^11.4.0": + version "11.4.0" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.4.0.tgz#293fc9d9a7a38b9aad8e9337e5014366c3b09ac0" + integrity sha512-Zx70bjE7LErRO9OaZrhf22Qye1y4F7iDl+ITjet0J+i+B88PrAOBkKvaAWhxsZf72tDLajwCgfCjJ2dvH77C3g== + dependencies: + "@emotion/memoize" "^0.7.4" + "@emotion/sheet" "^1.0.0" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + stylis "^4.0.3" + "@emotion/core@^10.0.9", "@emotion/core@^10.1.1": version "10.1.1" resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.1.1.tgz#c956c1365f2f2481960064bcb8c4732e5fb612c3" @@ -1626,6 +1702,14 @@ "@emotion/sheet" "0.9.4" "@emotion/utils" "0.11.3" +"@emotion/css-prettifier@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emotion/css-prettifier/-/css-prettifier-1.0.0.tgz#3ed4240d93c9798c001cedf27dd0aa960bdddd1a" + integrity sha512-efxSrRTiTqHTQVKW15Gz5H4pNAw8OqcG8NaiwkJIkqIdNXTD4Qr1zC1Ou6r2acd1oJJ2s56nb1ClnXMiWoj6gQ== + dependencies: + "@emotion/memoize" "^0.7.4" + stylis "^4.0.3" + "@emotion/css@^10.0.27", "@emotion/css@^10.0.9": version "10.0.27" resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c" @@ -1635,7 +1719,7 @@ "@emotion/utils" "0.11.3" babel-plugin-emotion "^10.0.27" -"@emotion/hash@0.8.0": +"@emotion/hash@0.8.0", "@emotion/hash@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== @@ -1652,6 +1736,17 @@ dependencies: "@emotion/memoize" "0.7.4" +"@emotion/jest@^11.3.0": + version "11.3.0" + resolved "https://registry.yarnpkg.com/@emotion/jest/-/jest-11.3.0.tgz#43bed6dcb47c8691b346cee231861ebc8f9b0016" + integrity sha512-LZqYc3yerhic1IvAcEwBLRs1DsUt3oY7Oz6n+e+HU32iYOK/vpfzlhgmQURE94BHfv6eCOj6DV38f3jSnIkBkQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@emotion/css-prettifier" "^1.0.0" + chalk "^4.1.0" + specificity "^0.4.1" + stylis "^4.0.3" + "@emotion/memoize@0.7.4": version "0.7.4" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" @@ -1662,6 +1757,24 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.6.6.tgz#004b98298d04c7ca3b4f50ca2035d4f60d2eed1b" integrity sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ== +"@emotion/memoize@^0.7.4", "@emotion/memoize@^0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" + integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== + +"@emotion/react@^11.4.0": + version "11.4.0" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.4.0.tgz#2465ad7b073a691409b88dfd96dc17097ddad9b7" + integrity sha512-4XklWsl9BdtatLoJpSjusXhpKv9YVteYKh9hPKP1Sxl+mswEFoUe0WtmtWjxEjkA51DQ2QRMCNOvKcSlCQ7ivg== + dependencies: + "@babel/runtime" "^7.13.10" + "@emotion/cache" "^11.4.0" + "@emotion/serialize" "^1.0.2" + "@emotion/sheet" "^1.0.1" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + hoist-non-react-statics "^3.3.1" + "@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16": version "0.11.16" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad" @@ -1683,11 +1796,27 @@ "@emotion/unitless" "^0.6.7" "@emotion/utils" "^0.8.2" +"@emotion/serialize@^1.0.0", "@emotion/serialize@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965" + integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A== + dependencies: + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.4" + "@emotion/unitless" "^0.7.5" + "@emotion/utils" "^1.0.0" + csstype "^3.0.2" + "@emotion/sheet@0.9.4": version "0.9.4" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA== +"@emotion/sheet@^1.0.0", "@emotion/sheet@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.0.1.tgz#245f54abb02dfd82326e28689f34c27aa9b2a698" + integrity sha512-GbIvVMe4U+Zc+929N1V7nW6YYJtidj31lidSmdYcWozwoBIObXBnaJkKNDjZrLm9Nc0BR+ZyHNaRZxqNZbof5g== + "@emotion/styled-base@^10.0.27": version "10.0.31" resolved "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.0.31.tgz#940957ee0aa15c6974adc7d494ff19765a2f742a" @@ -1716,7 +1845,7 @@ resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.7.1.tgz#50f63225e712d99e2b2b39c19c70fff023793ca5" integrity sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ== -"@emotion/unitless@0.7.5", "@emotion/unitless@^0.7.4": +"@emotion/unitless@0.7.5", "@emotion/unitless@^0.7.4", "@emotion/unitless@^0.7.5": version "0.7.5" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== @@ -1736,7 +1865,12 @@ resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc" integrity sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw== -"@emotion/weak-memoize@0.2.5": +"@emotion/utils@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af" + integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA== + +"@emotion/weak-memoize@0.2.5", "@emotion/weak-memoize@^0.2.5": version "0.2.5" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== @@ -7790,7 +7924,7 @@ babel-plugin-jest-hoist@^26.6.2: "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" -babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.8.0: +babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.6.1, babel-plugin-macros@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== @@ -9487,15 +9621,6 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -clipboard@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.4.tgz#836dafd66cf0fea5d71ce5d5b0bf6e958009112d" - integrity sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ== - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - cliui@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" @@ -10718,6 +10843,11 @@ csstype@^2.5.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.14.tgz#004822a4050345b55ad4dcc00be1d9cf2f4296de" integrity sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A== +csstype@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.7.tgz#2a5fb75e1015e84dd15692f71e89a1450290950b" + integrity sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g== + cucumber-expressions@^5.0.13: version "5.0.18" resolved "https://registry.yarnpkg.com/cucumber-expressions/-/cucumber-expressions-5.0.18.tgz#6c70779efd3aebc5e9e7853938b1110322429596" @@ -11589,11 +11719,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -delegate@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" - integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== - delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" @@ -14789,13 +14914,6 @@ gonzales-pe@^4.3.0: dependencies: minimist "^1.2.5" -good-listener@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" - integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA= - dependencies: - delegate "^3.1.2" - got@5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/got/-/got-5.6.0.tgz#bb1d7ee163b78082bbc8eb836f3f395004ea6fbf" @@ -15410,7 +15528,7 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.5, hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.5, hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -22172,8 +22290,6 @@ prismjs@1.24.0, prismjs@^1.22.0, prismjs@~1.23.0: version "1.24.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.0.tgz#0409c30068a6c52c89ef7f1089b3ca4de56be2ac" integrity sha512-SqV5GRsNqnzCL8k5dfAjCNhUrF3pR0A9lTDSCUZeh/LIshheXJEaP0hwLz2t4XHivd2J/v2HR+gRnigzeKe3cQ== - optionalDependencies: - clipboard "^2.0.0" private@^0.1.8, private@~0.1.5: version "0.1.8" @@ -24791,11 +24907,6 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -select@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" - integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= - selenium-webdriver@^4.0.0-alpha.7: version "4.0.0-alpha.7" resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.7.tgz#e3879d8457fd7ad8e4424094b7dc0540d99e6797" @@ -26207,6 +26318,11 @@ stylis@^3.5.0: resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe" integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q== +stylis@^4.0.3: + version "4.0.7" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.7.tgz#412a90c28079417f3d27c028035095e4232d2904" + integrity sha512-OFFeUXFgwnGOKvEXaSv0D0KQ5ADP0n6g3SVONx6I/85JzNZ3u50FRwB3lVIk1QO2HNdI75tbVzc4Z66Gdp9voA== + subarg@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" @@ -26828,11 +26944,6 @@ timsort@^0.3.0, timsort@~0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= -tiny-emitter@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c" - integrity sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow== - tiny-inflate@^1.0.0, tiny-inflate@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" From 65bc4a9c0e486f14b40b99d961644b2dd501ac16 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Fri, 9 Jul 2021 14:53:15 -0500 Subject: [PATCH 56/77] [Workplace Search] Remove users from groups views (#105108) * Remove user list from groups table * Remove users table from group overview * Lint --- .../groups/components/group_overview.tsx | 25 ++++++++----------- .../groups/components/group_row.test.tsx | 15 +---------- .../views/groups/components/group_row.tsx | 19 +------------- .../views/groups/components/groups_table.tsx | 7 ------ .../translations/translations/ja-JP.json | 2 -- .../translations/translations/zh-CN.json | 2 -- 6 files changed, 13 insertions(+), 57 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx index e7dfd6ddf13890..5714cc965827e1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx @@ -33,24 +33,23 @@ import { NAV, CANCEL_BUTTON } from '../../../constants'; import { USERS_AND_ROLES_PATH } from '../../../routes'; import { GroupLogic, MAX_NAME_LENGTH } from '../group_logic'; -import { GroupUsersTable } from './group_users_table'; - export const EMPTY_SOURCES_DESCRIPTION = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.overview.emptySourcesDescription', { defaultMessage: 'No content sources are shared with this group.', } ); -const GROUP_USERS_DESCRIPTION = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.groups.overview.groupUsersDescription', +const USERS_SECTION_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.groups.overview.usersSectionTitle', { - defaultMessage: 'Members will be able to search over the group’s sources.', + defaultMessage: 'Group users', } ); -export const EMPTY_USERS_DESCRIPTION = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.groups.overview.emptyUsersDescription', +const GROUP_USERS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.groups.overview.groupUsersDescription', { - defaultMessage: 'There are no users in this group.', + defaultMessage: + "Users assigned to this group gain access to the sources' data and content defined above. User assignments for this group can be managed in the Users and Roles area.", } ); const MANAGE_SOURCES_BUTTON_TEXT = i18n.translate( @@ -118,7 +117,7 @@ export const GroupOverview: React.FC = () => { onGroupNameInputChange, } = useActions(GroupLogic); const { - group: { name, contentSources, users, canDeleteGroup }, + group: { name, contentSources, canDeleteGroup }, groupNameInputValue, dataLoading, confirmDeleteModalVisible, @@ -158,7 +157,6 @@ export const GroupOverview: React.FC = () => { ); const hasContentSources = contentSources?.length > 0; - const hasUsers = users?.length > 0; const manageSourcesButton = ( @@ -199,12 +197,11 @@ export const GroupOverview: React.FC = () => { const usersSection = ( - {hasUsers && } + {manageUsersButton} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx index f98b873aed5bba..770bf8a51efd39 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx @@ -14,8 +14,7 @@ import moment from 'moment'; import { EuiTableRow } from '@elastic/eui'; -import { GroupRow, NO_USERS_MESSAGE, NO_SOURCES_MESSAGE } from './group_row'; -import { GroupUsers } from './group_users'; +import { GroupRow, NO_SOURCES_MESSAGE } from './group_row'; describe('GroupRow', () => { it('renders', () => { @@ -24,12 +23,6 @@ describe('GroupRow', () => { expect(wrapper.find(EuiTableRow)).toHaveLength(1); }); - it('renders group users', () => { - const wrapper = shallow(); - - expect(wrapper.find(GroupUsers)).toHaveLength(1); - }); - it('renders fromNow date string when in range', () => { const wrapper = shallow( @@ -44,12 +37,6 @@ describe('GroupRow', () => { expect(wrapper.find('small').text()).toEqual('Last updated January 1, 2020.'); }); - it('renders empty users message when no users present', () => { - const wrapper = shallow(); - - expect(wrapper.find('.user-group__accounts').text()).toEqual(NO_USERS_MESSAGE); - }); - it('renders empty sources message when no sources present', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx index 94d44fde57aedd..d079eb34fbf890 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx @@ -19,7 +19,6 @@ import { Group } from '../../../types'; import { MAX_NAME_LENGTH } from '../group_logic'; import { GroupSources } from './group_sources'; -import { GroupUsers } from './group_users'; const DAYS_CUTOFF = 8; export const NO_SOURCES_MESSAGE = i18n.translate( @@ -40,14 +39,7 @@ const dateDisplay = (date: string) => ? moment(date).fromNow() : moment(date).format('MMMM D, YYYY'); -export const GroupRow: React.FC = ({ - id, - name, - updatedAt, - contentSources, - users, - usersCount, -}) => { +export const GroupRow: React.FC = ({ id, name, updatedAt, contentSources }) => { const GROUP_UPDATED_TEXT = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.groupUpdatedText', { @@ -76,15 +68,6 @@ export const GroupRow: React.FC = ({ )}
- -
- {usersCount > 0 ? ( - - ) : ( - NO_USERS_MESSAGE - )} -
-
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx index cfb3ed80442358..45175e489f94a8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx @@ -36,12 +36,6 @@ const SOURCES_TABLE_HEADER = i18n.translate( defaultMessage: 'Content sources', } ); -const USERS_TABLE_HEADER = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.usersTableHeader', - { - defaultMessage: 'Users', - } -); export const GroupsTable: React.FC<{}> = () => { const { setActivePage } = useActions(GroupsLogic); @@ -77,7 +71,6 @@ export const GroupsTable: React.FC<{}> = () => { {GROUP_TABLE_HEADER} {SOURCES_TABLE_HEADER} - {USERS_TABLE_HEADER} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 11770d2d2f3866..b7cde09da6319d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8105,7 +8105,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.groupSourcesUpdated": "共有コンテンツソースが正常に更新されました。", "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.groupTableHeader": "グループ", "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.sourcesTableHeader": "コンテンツソース", - "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.usersTableHeader": "ユーザー", "xpack.enterpriseSearch.workplaceSearch.groups.groupUpdatedText": "前回更新日時{updatedAt}。", "xpack.enterpriseSearch.workplaceSearch.groups.heading": "グループを管理", "xpack.enterpriseSearch.workplaceSearch.groups.inviteUsers.action": "ユーザーを招待", @@ -8118,7 +8117,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveDescription": "グループはWorkplace Searchから削除されます。{name}を削除してよろしいですか?", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText": "確認", "xpack.enterpriseSearch.workplaceSearch.groups.overview.emptySourcesDescription": "コンテンツソースはこのグループと共有されていません。", - "xpack.enterpriseSearch.workplaceSearch.groups.overview.emptyUsersDescription": "このグループにはユーザーがありません。", "xpack.enterpriseSearch.workplaceSearch.groups.overview.groupSourcesDescription": "「{name}」グループのすべてのユーザーによって検索可能です。", "xpack.enterpriseSearch.workplaceSearch.groups.overview.groupSourcesTitle": "グループコンテンツソース", "xpack.enterpriseSearch.workplaceSearch.groups.overview.groupUsersDescription": "メンバーはグループのソースを検索できます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9704070feb8ab0..ac43983a75ccfb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8173,7 +8173,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.groupSourcesUpdated": "已成功更新共享内容源。", "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.groupTableHeader": "组", "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.sourcesTableHeader": "内容源", - "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.usersTableHeader": "用户", "xpack.enterpriseSearch.workplaceSearch.groups.groupUpdatedText": "上次更新于 {updatedAt}。", "xpack.enterpriseSearch.workplaceSearch.groups.heading": "管理组", "xpack.enterpriseSearch.workplaceSearch.groups.inviteUsers.action": "邀请用户", @@ -8186,7 +8185,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveDescription": "您的组将从 Workplace Search 中删除。确定要移除 {name}?", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText": "确认", "xpack.enterpriseSearch.workplaceSearch.groups.overview.emptySourcesDescription": "未与此组共享任何内容源。", - "xpack.enterpriseSearch.workplaceSearch.groups.overview.emptyUsersDescription": "此组中没有用户。", "xpack.enterpriseSearch.workplaceSearch.groups.overview.groupSourcesDescription": "可按“{name}”组中的所有用户搜索。", "xpack.enterpriseSearch.workplaceSearch.groups.overview.groupSourcesTitle": "组内容源", "xpack.enterpriseSearch.workplaceSearch.groups.overview.groupUsersDescription": "成员将可以对该组的源进行搜索。", From f3228a38f41e7f946883030f2a718637ffd3c514 Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 9 Jul 2021 13:26:26 -0700 Subject: [PATCH 57/77] [kbn/client/ui-settings] support using uiSettings in a specific space (#105116) Co-authored-by: spalger --- .../kbn_client/kbn_client_requester.test.ts | 35 +++++++++++++++++++ .../src/kbn_client/kbn_client_requester.ts | 13 +++++++ .../src/kbn_client/kbn_client_ui_settings.ts | 25 +++++++------ 3 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 packages/kbn-test/src/kbn_client/kbn_client_requester.test.ts diff --git a/packages/kbn-test/src/kbn_client/kbn_client_requester.test.ts b/packages/kbn-test/src/kbn_client/kbn_client_requester.test.ts new file mode 100644 index 00000000000000..bb2f923ad1f01f --- /dev/null +++ b/packages/kbn-test/src/kbn_client/kbn_client_requester.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pathWithSpace } from './kbn_client_requester'; + +describe('pathWithSpace()', () => { + it('adds a space to the path', () => { + expect(pathWithSpace('hello')`/foo/bar`).toMatchInlineSnapshot(`"/s/hello/foo/bar"`); + }); + + it('ignores the space when it is empty', () => { + expect(pathWithSpace(undefined)`/foo/bar`).toMatchInlineSnapshot(`"/foo/bar"`); + expect(pathWithSpace('')`/foo/bar`).toMatchInlineSnapshot(`"/foo/bar"`); + }); + + it('ignores the space when it is the default space', () => { + expect(pathWithSpace('default')`/foo/bar`).toMatchInlineSnapshot(`"/foo/bar"`); + }); + + it('uriencodes variables in the path', () => { + expect(pathWithSpace('space')`hello/${'funky/username🏴‍☠️'}`).toMatchInlineSnapshot( + `"/s/space/hello/funky%2Fusername%F0%9F%8F%B4%E2%80%8D%E2%98%A0%EF%B8%8F"` + ); + }); + + it('ensures the path always starts with a slash', () => { + expect(pathWithSpace('foo')`hello/world`).toMatchInlineSnapshot(`"/s/foo/hello/world"`); + expect(pathWithSpace()`hello/world`).toMatchInlineSnapshot(`"/hello/world"`); + }); +}); diff --git a/packages/kbn-test/src/kbn_client/kbn_client_requester.ts b/packages/kbn-test/src/kbn_client/kbn_client_requester.ts index a194b593b3863a..c2e4247df1ab0f 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_requester.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_requester.ts @@ -23,6 +23,19 @@ const isIgnorableError = (error: any, ignorableErrors: number[] = []) => { return isAxiosResponseError(error) && ignorableErrors.includes(error.response.status); }; +/** + * Creates a template literal tag which will uriencode the variables in a template literal + * as well as prefix the path with a specific space if one is defined + */ +export const pathWithSpace = (space?: string) => { + const prefix = !space || space === 'default' ? '' : uriencode`/s/${space}`; + + return (strings: TemplateStringsArray, ...args: Array) => { + const path = uriencode(strings, ...args); + return path.startsWith('/') || path === '' ? `${prefix}${path}` : `${prefix}/${path}`; + }; +}; + export const uriencode = ( strings: TemplateStringsArray, ...values: Array diff --git a/packages/kbn-test/src/kbn_client/kbn_client_ui_settings.ts b/packages/kbn-test/src/kbn_client/kbn_client_ui_settings.ts index 78155098ef0388..7ea685667d48bf 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_ui_settings.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_ui_settings.ts @@ -8,7 +8,7 @@ import { ToolingLog } from '@kbn/dev-utils'; -import { KbnClientRequester, uriencode } from './kbn_client_requester'; +import { KbnClientRequester, pathWithSpace } from './kbn_client_requester'; export type UiSettingValues = Record; interface UiSettingsApiResponse { @@ -27,8 +27,8 @@ export class KbnClientUiSettings { private readonly defaults?: UiSettingValues ) {} - async get(setting: string) { - const all = await this.getAll(); + async get(setting: string, { space }: { space?: string } = {}) { + const all = await this.getAll({ space }); const value = all[setting]?.userValue; this.log.verbose('uiSettings.value: %j', value); @@ -45,9 +45,9 @@ export class KbnClientUiSettings { /** * Unset a uiSetting */ - async unset(setting: string) { + async unset(setting: string, { space }: { space?: string } = {}) { const { data } = await this.requester.request({ - path: uriencode`/api/kibana/settings/${setting}`, + path: pathWithSpace(space)`/api/kibana/settings/${setting}`, method: 'DELETE', }); return data; @@ -57,7 +57,10 @@ export class KbnClientUiSettings { * Replace all uiSettings with the `doc` values, `doc` is merged * with some defaults */ - async replace(doc: UiSettingValues, { retries = 5 }: { retries?: number } = {}) { + async replace( + doc: UiSettingValues, + { retries = 5, space }: { retries?: number; space?: string } = {} + ) { this.log.debug('replacing kibana config doc: %j', doc); const changes: Record = { @@ -73,7 +76,7 @@ export class KbnClientUiSettings { await this.requester.request({ method: 'POST', - path: '/api/kibana/settings', + path: pathWithSpace(space)`/api/kibana/settings`, body: { changes }, retries, }); @@ -82,11 +85,11 @@ export class KbnClientUiSettings { /** * Add fields to the config doc (like setting timezone and defaultIndex) */ - async update(updates: UiSettingValues) { + async update(updates: UiSettingValues, { space }: { space?: string } = {}) { this.log.debug('applying update to kibana config: %j', updates); await this.requester.request({ - path: '/api/kibana/settings', + path: pathWithSpace(space)`/api/kibana/settings`, method: 'POST', body: { changes: updates, @@ -95,9 +98,9 @@ export class KbnClientUiSettings { }); } - private async getAll() { + private async getAll({ space }: { space?: string } = {}) { const { data } = await this.requester.request({ - path: '/api/kibana/settings', + path: pathWithSpace(space)`/api/kibana/settings`, method: 'GET', }); From 36bf7f71208d1c03eb4f06fa61aa75f0095fc298 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Fri, 9 Jul 2021 14:28:02 -0600 Subject: [PATCH 58/77] [Maps] Fix tracking threshold alerts improper handling of elasticsearch epoch millis strings (#105010) --- .../alert_types/geo_containment/es_query_builder.ts | 9 ++++++++- .../alert_types/geo_containment/geo_containment.ts | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts index 1e26ea09618d5a..37e0a293b03a00 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts @@ -151,7 +151,14 @@ export async function executeEsQueryFactory( }, }, ], - docvalue_fields: [entity, dateField, geoField], + docvalue_fields: [ + entity, + { + field: dateField, + format: 'strict_date_optional_time', + }, + geoField, + ], _source: false, }, }, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts index 754af920b009e5..21a536dd474bad 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts @@ -103,7 +103,7 @@ export function getActiveEntriesAndGenerateAlerts( locationsArr.forEach(({ location, shapeLocationId, dateInShape, docId }) => { const context = { entityId: entityName, - entityDateTime: dateInShape ? new Date(dateInShape).toISOString() : null, + entityDateTime: dateInShape || null, entityDocumentId: docId, detectionDateTime: new Date(currIntervalEndTime).toISOString(), entityLocation: `POINT (${location[0]} ${location[1]})`, From f7b87a5f65da7ed8538a962ccc8f2ca48aa18799 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Fri, 9 Jul 2021 15:33:20 -0500 Subject: [PATCH 59/77] [ML] Fix Single Metric Viewer & Explorer annotation table actions overflow and annotations count not matching (#104955) * Fix annotations * Fix translations * Fix onclick open * Fix label/aggregations mismatch * Fix title --- .../annotations_table/annotations_table.js | 136 ++++++++---------- .../public/application/explorer/explorer.js | 36 +++-- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 4 files changed, 89 insertions(+), 85 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index ed603357206ad9..39d4dd1a71dd9f 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -18,13 +18,13 @@ import React, { Component, Fragment, useContext } from 'react'; import memoizeOne from 'memoize-one'; import { EuiBadge, - EuiButtonEmpty, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiLink, EuiLoadingSpinner, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -52,6 +52,19 @@ import { timeFormatter } from '../../../../../common/util/date_utils'; import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context'; import { DatafeedChartFlyout } from '../../../jobs/jobs_list/components/datafeed_chart_flyout'; +const editAnnotationsText = ( + +); +const viewDataFeedText = ( + +); + const CURRENT_SERIES = 'current_series'; /** * Table component for rendering the lists of annotations for an ML job. @@ -463,82 +476,67 @@ class AnnotationsTableUI extends Component { const actions = []; actions.push({ - render: (annotation) => { - // find the original annotation because the table might not show everything + name: editAnnotationsText, + description: editAnnotationsText, + icon: 'pencil', + type: 'icon', + onClick: (annotation) => { const annotationId = annotation._id; const originalAnnotation = annotations.find((d) => d._id === annotationId); - const editAnnotationsText = ( - - ); - const editAnnotationsAriaLabelText = i18n.translate( - 'xpack.ml.annotationsTable.editAnnotationsTooltipAriaLabel', - { defaultMessage: 'Edit annotation' } - ); - return ( - annotationUpdatesService.setValue(originalAnnotation ?? annotation)} - > - {editAnnotationsText} - - ); + + annotationUpdatesService.setValue(originalAnnotation ?? annotation); }, }); if (this.state.jobId && this.props.jobs[0].analysis_config.bucket_span) { // add datafeed modal action actions.push({ - render: (annotation) => { - const viewDataFeedText = ( - - ); - const viewDataFeedTooltipAriaLabelText = i18n.translate( - 'xpack.ml.annotationsTable.datafeedChartTooltipAriaLabel', - { defaultMessage: 'Datafeed chart' } - ); - return ( - - this.setState({ - datafeedFlyoutVisible: true, - datafeedEnd: annotation.end_timestamp, - }) - } - > - {viewDataFeedText} - - ); + name: viewDataFeedText, + description: viewDataFeedText, + icon: 'visAreaStacked', + type: 'icon', + onClick: (annotation) => { + this.setState({ + datafeedFlyoutVisible: true, + datafeedEnd: annotation.end_timestamp, + }); }, }); } if (isSingleMetricViewerLinkVisible) { actions.push({ - render: (annotation) => { + name: (annotation) => { const isDrillDownAvailable = isTimeSeriesViewJob(this.getJob(annotation.job_id)); - const openInSingleMetricViewerTooltipText = isDrillDownAvailable ? ( - - ) : ( - + + if (isDrillDownAvailable) { + return ( + + ); + } + return ( + + } + > + + ); - const openInSingleMetricViewerAriaLabelText = isDrillDownAvailable + }, + description: (annotation) => { + const isDrillDownAvailable = isTimeSeriesViewJob(this.getJob(annotation.job_id)); + + return isDrillDownAvailable ? i18n.translate('xpack.ml.annotationsTable.openInSingleMetricViewerAriaLabel', { defaultMessage: 'Open in Single Metric Viewer', }) @@ -546,19 +544,11 @@ class AnnotationsTableUI extends Component { 'xpack.ml.annotationsTable.jobConfigurationNotSupportedInSingleMetricViewerAriaLabel', { defaultMessage: 'Job configuration not supported in Single Metric Viewer' } ); - - return ( - this.openSingleMetricView(annotation)} - > - {openInSingleMetricViewerTooltipText} - - ); }, + enabled: (annotation) => isTimeSeriesViewJob(this.getJob(annotation.job_id)), + icon: 'visLine', + type: 'icon', + onClick: (annotation) => this.openSingleMetricView(annotation), }); } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index a11a42b9b65b2c..31058b62af7fe5 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -261,6 +261,30 @@ export class ExplorerUI extends React.Component { } = this.props.explorerState; const { annotationsData, aggregations, error: annotationsError } = annotations; + const annotationsCnt = Array.isArray(annotationsData) ? annotationsData.length : 0; + const allAnnotationsCnt = Array.isArray(aggregations?.event?.buckets) + ? aggregations.event.buckets.reduce((acc, v) => acc + v.doc_count, 0) + : annotationsCnt; + + const badge = + allAnnotationsCnt > annotationsCnt ? ( + + + + ) : ( + + + + ); + const jobSelectorProps = { dateFormatTz: getDateFormatTz(), }; @@ -404,7 +428,7 @@ export class ExplorerUI extends React.Component { {loading === false && tableData.anomalies?.length ? ( ) : null} - {annotationsData.length > 0 && ( + {annotationsCnt > 0 && ( <> - - - ), + badge, }} /> diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b7cde09da6319d..387547295f0470 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13639,7 +13639,6 @@ "xpack.ml.annotationsTable.byColumnSMVName": "グループ基準", "xpack.ml.annotationsTable.detectorColumnName": "検知器", "xpack.ml.annotationsTable.editAnnotationsTooltip": "注釈を編集します", - "xpack.ml.annotationsTable.editAnnotationsTooltipAriaLabel": "注釈を編集します", "xpack.ml.annotationsTable.eventColumnName": "イベント", "xpack.ml.annotationsTable.fromColumnName": "開始:", "xpack.ml.annotationsTable.howToCreateAnnotationDescription": "注釈を作成するには、{linkToSingleMetricView} を開きます", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ac43983a75ccfb..d142969022c81a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13818,7 +13818,6 @@ "xpack.ml.annotationsTable.byColumnSMVName": "依据", "xpack.ml.annotationsTable.detectorColumnName": "检测工具", "xpack.ml.annotationsTable.editAnnotationsTooltip": "编辑注释", - "xpack.ml.annotationsTable.editAnnotationsTooltipAriaLabel": "编辑注释", "xpack.ml.annotationsTable.eventColumnName": "事件", "xpack.ml.annotationsTable.fromColumnName": "自", "xpack.ml.annotationsTable.howToCreateAnnotationDescription": "要创建注释,请打开 {linkToSingleMetricView}", From ce48b73dc8f215eb7761f896b1bdaa4d584e5121 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 9 Jul 2021 13:59:56 -0700 Subject: [PATCH 60/77] skip suites failing es promotion (#104466) --- test/functional/apps/discover/_data_grid_field_data.ts | 3 ++- test/functional/apps/discover/_field_data.ts | 7 ++++--- .../apps/discover/_field_data_with_fields_api.ts | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/test/functional/apps/discover/_data_grid_field_data.ts b/test/functional/apps/discover/_data_grid_field_data.ts index 94e8e942f86ba2..cf8a6fc1bd60f2 100644 --- a/test/functional/apps/discover/_data_grid_field_data.ts +++ b/test/functional/apps/discover/_data_grid_field_data.ts @@ -19,7 +19,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; const dataGrid = getService('dataGrid'); - describe('discover data grid field data tests', function describeIndexTests() { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104466 + describe.skip('discover data grid field data tests', function describeIndexTests() { this.tags('includeFirefox'); before(async function () { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); diff --git a/test/functional/apps/discover/_field_data.ts b/test/functional/apps/discover/_field_data.ts index 5ab6495686726c..6471b751945a80 100644 --- a/test/functional/apps/discover/_field_data.ts +++ b/test/functional/apps/discover/_field_data.ts @@ -20,7 +20,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); const find = getService('find'); - describe('discover tab', function describeIndexTests() { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104466 + describe.skip('discover tab', function describeIndexTests() { this.tags('includeFirefox'); before(async function () { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); @@ -33,8 +34,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); }); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104466 - describe.skip('field data', function () { + + describe('field data', function () { it('search php should show the correct hit count', async function () { const expectedHitCount = '445'; await retry.try(async function () { diff --git a/test/functional/apps/discover/_field_data_with_fields_api.ts b/test/functional/apps/discover/_field_data_with_fields_api.ts index 110e255d18c756..7c6867e9350634 100644 --- a/test/functional/apps/discover/_field_data_with_fields_api.ts +++ b/test/functional/apps/discover/_field_data_with_fields_api.ts @@ -20,7 +20,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); const find = getService('find'); - describe('discover tab with new fields API', function describeIndexTests() { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104466 + describe.skip('discover tab with new fields API', function describeIndexTests() { this.tags('includeFirefox'); before(async function () { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); From fdd37e3cfb0116772c48fc45c75e8fe61cd55579 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Fri, 9 Jul 2021 17:14:37 -0400 Subject: [PATCH 61/77] Fixed description on Ent Home (#105122) --- .../product_selector/product_selector.tsx | 10 ++++++---- .../applications/enterprise_search/index.scss | 14 -------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx index a7eb2424e797aa..0dd2b0988b3f4c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx @@ -19,6 +19,7 @@ import { EuiFlexItem, EuiSpacer, EuiTitle, + EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -59,14 +60,15 @@ export const ProductSelector: React.FC = ({ access }) => { -

+

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

- -

+ + +

{config.host ? i18n.translate('xpack.enterpriseSearch.overview.subheading', { defaultMessage: 'Select a product to get started.', @@ -75,7 +77,7 @@ export const ProductSelector: React.FC = ({ access }) => { defaultMessage: 'Choose a product to set up and get started.', })}

-
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.scss b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.scss index 45bf37def11216..4be8d7322b4c8e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.scss +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.scss @@ -21,21 +21,7 @@ &__header { text-align: center; margin: auto; - } - - &__heading { - @include euiBreakpoint('xs', 's') { - font-size: $euiFontSizeXL; - line-height: map-get(map-get($euiTitles, 'm'), 'line-height'); - } - } - - &__subheading { - color: $euiColorMediumShade; - font-size: $euiFontSize; - @include euiBreakpoint('m', 'l', 'xl') { - font-size: $euiFontSizeL; margin-bottom: $euiSizeL; } } From b7f50c279bf9e348096f2d7b9e289ec1acb5ae47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ece=20=C3=96zalp?= Date: Fri, 9 Jul 2021 17:17:48 -0400 Subject: [PATCH 62/77] [CTI] updates overview cti panel copy (#105074) --- .../overview/components/overview_cti_links/translations.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts index 663ec3a75c9025..91abd48eb2b7e8 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts @@ -51,9 +51,10 @@ export const DANGER_TITLE = i18n.translate( ); export const DANGER_BODY = i18n.translate( - 'xpack.securitySolution.overview.ctiDashboardDangerPanelBody', + 'xpack.securitySolution.overview.ctiDashboardEnableThreatIntel', { - defaultMessage: 'You need to enable module in order to view data from different sources.', + defaultMessage: + 'You need to enable the filebeat threatintel module in order to view data from different sources.', } ); From bcc8ee2532133c39e663f73303187a2345ef24a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ece=20=C3=96zalp?= Date: Fri, 9 Jul 2021 17:28:15 -0400 Subject: [PATCH 63/77] [CTI] makes dashboard links external (#104979) * [CTI] makes dashboard links external --- .../cti_disabled_module.tsx | 2 +- .../threat_intel_panel_view.tsx | 28 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx index e22fec1861f8bf..21a4beca72f3b2 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx @@ -25,7 +25,7 @@ export const CtiDisabledModuleComponent = () => { title={i18n.DANGER_TITLE} body={i18n.DANGER_BODY} button={ - + {i18n.DANGER_BUTTON} } diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx index 4565c16bc2bf65..b34f6e657d39a9 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx @@ -22,7 +22,6 @@ import { InspectButtonContainer } from '../../../common/components/inspect'; import { HeaderSection } from '../../../common/components/header_section'; import { ID as CTIEventCountQueryId } from '../../containers/overview_cti_links/use_cti_event_counts'; import { CtiListItem } from '../../containers/overview_cti_links/helpers'; -import { LinkButton } from '../../../common/components/links'; import { useKibana } from '../../../common/lib/kibana'; import { CtiInnerPanel } from './cti_inner_panel'; import * as i18n from './translations'; @@ -36,7 +35,7 @@ const DashboardLinkItems = styled(EuiFlexGroup)` `; const Title = styled(EuiFlexItem)` - min-width: 140px; + min-width: 110px; `; const List = styled.ul` @@ -45,12 +44,11 @@ const List = styled.ul` const DashboardRightSideElement = styled(EuiFlexItem)` align-items: flex-end; - max-width: 160px; `; const RightSideLink = styled(EuiLink)` text-align: right; - min-width: 140px; + min-width: 180px; `; interface ThreatIntelPanelViewProps { @@ -96,12 +94,12 @@ export const ThreatIntelPanelView: React.FC = ({ const button = useMemo( () => ( - + - + ), [buttonHref] ); @@ -117,7 +115,11 @@ export const ThreatIntelPanelView: React.FC = ({ color={'primary'} title={i18n.INFO_TITLE} body={i18n.INFO_BODY} - button={{i18n.INFO_BUTTON}} + button={ + + {i18n.INFO_BUTTON} + + } /> ) : null, [isDashboardPluginDisabled, threatIntelDashboardDocLink] @@ -149,9 +151,7 @@ export const ThreatIntelPanelView: React.FC = ({ gutterSize="l" justifyContent="spaceBetween" > - - {title} - + {title} = ({ alignItems="center" justifyContent="flexEnd" > - + {count} - + {path ? ( - {linkCopy} + + {linkCopy} + ) : ( {linkCopy} From 1739b55f94df04b4b99fbc11233cc16d5cef863b Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 9 Jul 2021 14:31:01 -0700 Subject: [PATCH 64/77] [Enterprise Search] Fix Error Connecting view not displaying for auth issues (#105125) * Fix ent-search authentication to show the error connecting screen Missed this in #103555 * [Misc] updoot handleConnectionError order/spacing to match - why? because i've lost control of my life, probably --- .../server/lib/enterprise_search_request_handler.test.ts | 2 +- .../server/lib/enterprise_search_request_handler.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts index d0e74f3234c146..f9756119b336cd 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts @@ -403,7 +403,7 @@ describe('EnterpriseSearchRequestHandler', () => { expect(responseMock.customError).toHaveBeenCalledWith({ statusCode: 502, body: 'Cannot authenticate Enterprise Search user', - headers: mockExpectedResponseHeaders, + headers: { ...mockExpectedResponseHeaders, [ERROR_CONNECTING_HEADER]: 'true' }, }); expect(mockLogger.error).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts index 8031fc724f7b37..57b91c2b30c73f 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts @@ -283,12 +283,11 @@ export class EnterpriseSearchRequestHandler { handleConnectionError(response: KibanaResponseFactory, e: Error) { const errorMessage = `Error connecting to Enterprise Search: ${e?.message || e.toString()}`; + const headers = { ...this.headers, [ERROR_CONNECTING_HEADER]: 'true' }; this.log.error(errorMessage); if (e instanceof Error) this.log.debug(e.stack as string); - const headers = { ...this.headers, [ERROR_CONNECTING_HEADER]: 'true' }; - return response.customError({ statusCode: 502, headers, body: errorMessage }); } @@ -298,9 +297,10 @@ export class EnterpriseSearchRequestHandler { */ handleAuthenticationError(response: KibanaResponseFactory) { const errorMessage = 'Cannot authenticate Enterprise Search user'; + const headers = { ...this.headers, [ERROR_CONNECTING_HEADER]: 'true' }; this.log.error(errorMessage); - return response.customError({ statusCode: 502, headers: this.headers, body: errorMessage }); + return response.customError({ statusCode: 502, headers, body: errorMessage }); } /** From a05853ab2ad26e5f8fe8654a138c805ba45422be Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Fri, 9 Jul 2021 17:52:17 -0400 Subject: [PATCH 65/77] [APM] Support for data streams index patterns for cloud migration (#105015) * [APM] Support for data streams index patterns for cloud migration (#101095) * [APM] Update apm package version to 0.3.0 (elastic/apm-server/#5579) --- x-pack/plugins/apm/server/index.test.ts | 2 +- x-pack/plugins/apm/server/index.ts | 6 ++- .../get_apm_package_policy_definition.ts | 2 +- .../create_static_index_pattern.ts | 5 ++- x-pack/plugins/apm/server/routes/fleet.ts | 41 ++++++++++++++----- 5 files changed, 41 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/apm/server/index.test.ts b/x-pack/plugins/apm/server/index.test.ts index 6052ec921f9f9c..56c9825db5a5cd 100644 --- a/x-pack/plugins/apm/server/index.test.ts +++ b/x-pack/plugins/apm/server/index.test.ts @@ -30,7 +30,7 @@ describe('mergeConfigs', () => { expect(mergeConfigs(apmOssConfig, apmConfig)).toEqual({ 'apm_oss.errorIndices': 'logs-apm*,apm-*-error-*', - 'apm_oss.indexPattern': 'apm-*', + 'apm_oss.indexPattern': 'traces-apm*,logs-apm*,metrics-apm*,apm-*', 'apm_oss.metricsIndices': 'metrics-apm*,apm-*-metric-*', 'apm_oss.spanIndices': 'traces-apm*,apm-*-span-*', 'apm_oss.transactionIndices': 'traces-apm*,apm-*-transaction-*', diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index f14894a76edb45..9031f454f4a7fb 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -74,7 +74,7 @@ export function mergeConfigs( 'apm_oss.metricsIndices': apmOssConfig.metricsIndices, 'apm_oss.sourcemapIndices': apmOssConfig.sourcemapIndices, 'apm_oss.onboardingIndices': apmOssConfig.onboardingIndices, - 'apm_oss.indexPattern': apmOssConfig.indexPattern, // TODO: add data stream indices: traces-apm*,logs-apm*,metrics-apm*. Blocked by https://github.com/elastic/kibana/issues/87851 + 'apm_oss.indexPattern': apmOssConfig.indexPattern, /* eslint-enable @typescript-eslint/naming-convention */ 'xpack.apm.serviceMapEnabled': apmConfig.serviceMapEnabled, 'xpack.apm.serviceMapFingerprintBucketSize': @@ -119,6 +119,10 @@ export function mergeConfigs( 'apm_oss.metricsIndices' ] = `metrics-apm*,${mergedConfig['apm_oss.metricsIndices']}`; + mergedConfig[ + 'apm_oss.indexPattern' + ] = `traces-apm*,logs-apm*,metrics-apm*,${mergedConfig['apm_oss.indexPattern']}`; + return mergedConfig; } diff --git a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts index 82e85e7da9bb3d..291b2fa2af99d8 100644 --- a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts +++ b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts @@ -34,7 +34,7 @@ export function getApmPackagePolicyDefinition( ], package: { name: APM_PACKAGE_NAME, - version: '0.3.0-dev.1', + version: '0.3.0', title: 'Elastic APM', }, }; diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts index 607a7e6227a9d6..a2944d6241d2d9 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts @@ -19,7 +19,8 @@ export async function createStaticIndexPattern( setup: Setup, config: APMRouteHandlerResources['config'], savedObjectsClient: InternalSavedObjectsClient, - spaceId: string | undefined + spaceId: string | undefined, + overwrite = false ): Promise { return withApmSpan('create_static_index_pattern', async () => { // don't autocreate APM index pattern if it's been disabled via the config @@ -45,7 +46,7 @@ export async function createStaticIndexPattern( }, { id: APM_STATIC_INDEX_PATTERN_ID, - overwrite: false, + overwrite, namespace: spaceId, } ) diff --git a/x-pack/plugins/apm/server/routes/fleet.ts b/x-pack/plugins/apm/server/routes/fleet.ts index 6628d29b256f7e..b760014d6af89f 100644 --- a/x-pack/plugins/apm/server/routes/fleet.ts +++ b/x-pack/plugins/apm/server/routes/fleet.ts @@ -25,6 +25,8 @@ import { createCloudApmPackgePolicy } from '../lib/fleet/create_cloud_apm_packag import { getUnsupportedApmServerSchema } from '../lib/fleet/get_unsupported_apm_server_schema'; import { isSuperuser } from '../lib/fleet/is_superuser'; import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { createStaticIndexPattern } from '../lib/index_pattern/create_static_index_pattern'; const hasFleetDataRoute = createApmServerRoute({ endpoint: 'GET /api/apm/fleet/has_data', @@ -154,7 +156,7 @@ const createCloudApmPackagePolicyRoute = createApmServerRoute({ endpoint: 'POST /api/apm/fleet/cloud_apm_package_policy', options: { tags: ['access:apm', 'access:apm_write'] }, handler: async (resources) => { - const { plugins, context, config, request, logger } = resources; + const { plugins, context, config, request, logger, core } = resources; const cloudApmMigrationEnabled = config['xpack.apm.agent.migrations.enabled']; if (!plugins.fleet || !plugins.security) { @@ -171,15 +173,34 @@ const createCloudApmPackagePolicyRoute = createApmServerRoute({ if (!hasRequiredRole || !cloudApmMigrationEnabled) { throw Boom.forbidden(CLOUD_SUPERUSER_REQUIRED_MESSAGE); } - return { - cloud_apm_package_policy: await createCloudApmPackgePolicy({ - cloudPluginSetup, - fleetPluginStart, - savedObjectsClient, - esClient, - logger, - }), - }; + + const cloudApmAackagePolicy = await createCloudApmPackgePolicy({ + cloudPluginSetup, + fleetPluginStart, + savedObjectsClient, + esClient, + logger, + }); + + const [setup, internalSavedObjectsClient] = await Promise.all([ + setupRequest(resources), + core + .start() + .then(({ savedObjects }) => savedObjects.createInternalRepository()), + ]); + + const spaceId = plugins.spaces?.setup.spacesService.getSpaceId(request); + + // force update the index pattern title with data streams + await createStaticIndexPattern( + setup, + config, + internalSavedObjectsClient, + spaceId, + true + ); + + return { cloud_apm_package_policy: cloudApmAackagePolicy }; }, }); From 937a4f381a592f605b1edc6a898de02e4831aadf Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Fri, 9 Jul 2021 17:20:10 -0500 Subject: [PATCH 66/77] [Security Solution] Enrichment details UI cleanup (#104995) * Remove the "view threat intel data" button from the alert summary This can be accomplished by clicking the tab itself; there's no real need for this button. * Remove section title from alert summary view This made sense when we had both alert and threat sections, but we no longer do. Removes the corresponding translation, and the analogously unused title from the defunct threat summary view. * Smaller spacer on alert summary tab This is distractingly large as compared to other tabs. * Move "no enrichments" panel below our threat details table * Remove old import * Move inspect button inline with rest of header * Add HR separator to top of NoEnrichmentsPanel This should arguably be added a level above so as to keep this panel context-agnostic, but it's currently only used in one place and will always require the HR, so YAGNI for now. * Adds more space between title and description on "no data" panel It has been suggested that the NoEnrichmentsPanel should be following the guidelines of the EuiEmptyPrompt. If we end up needing e.g. centered text, we're better off rewriting NoEnrichmentsPanelView in terms of an EuiEmptyPrompt. * StyledEuiInMemoryTable has no header row height We have never provided column names to this component. However, there is default padding on the thead tds such that even without content they take up vertical height. This has resulted in some extra top-margin on historical uses of this table (which are just the Alert Details views). However, the addition of a sibling table (ThreatSummaryView) made the extra margin noticable, since it made the two tables appear disjointed even though they're right up against each other. This fixes the issue by removing the padding, allowing the thead to take no height. And now that that space isn't taken up by the table header, we need to add a little bit of space between the header and table on the Threat Details view. * Move test to appropriate location The ThreatDetailsView is no longer responsible for displaying the "no data" components, that's now a level above in EventDetails. * Prune unused translations These have been changed in the latest designs. * Only add HR if panel is preceded by enrichments We do not want an HR if there's nothing above the panel. --- .../event_details/alert_summary_view.tsx | 2 +- .../empty_threat_details_view.test.tsx | 48 ---------- .../cti_details/empty_threat_details_view.tsx | 51 ----------- .../cti_details/no_enrichments_panel.test.tsx | 57 ++++++++++++ .../cti_details/no_enrichments_panel.tsx | 88 +++++++++++++++++++ .../cti_details/threat_details_view.test.tsx | 9 -- .../cti_details/threat_details_view.tsx | 18 ++-- .../event_details/cti_details/translations.ts | 34 ++++++- .../event_details/event_details.test.tsx | 7 ++ .../event_details/event_details.tsx | 53 +++++------ .../components/event_details/summary_view.tsx | 7 +- .../components/event_details/translations.ts | 12 --- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 14 files changed, 222 insertions(+), 170 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx index 3a1a29b63eadf0..329b8e32f057d8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx @@ -213,7 +213,7 @@ const AlertSummaryViewComponent: React.FC<{ return ( <> - + { - const mount = useMountAppended(); - const mockTheme = getMockTheme({ - eui: { - euiBreakpoints: { - l: '1200px', - }, - paddingSizes: { - m: '8px', - xl: '32px', - }, - }, - }); - - test('renders correct items', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="empty-threat-details-view"]').exists()).toEqual(true); - }); - - test('renders link to docs', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('a').exists()).toEqual(true); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.tsx deleted file mode 100644 index d7e1c4d7754ecd..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiLink, EuiSpacer, EuiTitle } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { useKibana } from '../../../lib/kibana'; -import * as i18n from './translations'; - -const EmptyThreatDetailsViewContainer = styled.div` - display: flex; - flex-direction: column; - align-items: center; -`; - -const Span = styled.span` - color: ${({ theme }) => theme.eui.euiColorDarkShade}; - line-height: 1.8em; - text-align: center; - padding: ${({ theme }) => `${theme.eui.paddingSizes.m} ${theme.eui.paddingSizes.xl}`}; -`; - -const EmptyThreatDetailsViewComponent: React.FC<{}> = () => { - const threatIntelDocsUrl = `${ - useKibana().services.docLinks.links.filebeat.base - }/filebeat-module-threatintel.html`; - - return ( - - - -

{i18n.NO_ENRICHMENT_FOUND}

-
- - {i18n.IF_CTI_NOT_ENABLED} - - {i18n.CHECK_DOCS} - - -
- ); -}; - -EmptyThreatDetailsViewComponent.displayName = 'EmptyThreatDetailsView'; - -export const EmptyThreatDetailsView = React.memo(EmptyThreatDetailsViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.test.tsx new file mode 100644 index 00000000000000..819c666bd7267e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { NoEnrichmentsPanel } from './no_enrichments_panel'; +import * as i18n from './translations'; + +jest.mock('../../../lib/kibana'); + +describe('NoEnrichmentsPanelView', () => { + it('renders a qualified container', () => { + const wrapper = mount( + + ); + expect(wrapper.find('[data-test-subj="no-enrichments-panel"]').exists()).toEqual(true); + }); + + it('renders nothing when all enrichments are present', () => { + const wrapper = mount( + + ); + expect(wrapper.find('[data-test-subj="no-enrichments-panel"]').exists()).toEqual(false); + }); + + it('renders expected text when no enrichments are present', () => { + const wrapper = mount( + + ); + expect(wrapper.find('[data-test-subj="no-enrichments-panel"]').hostNodes().text()).toContain( + i18n.NO_ENRICHMENTS_FOUND_TITLE + ); + }); + + it('renders expected text when existing enrichments are absent', () => { + const wrapper = mount( + + ); + expect(wrapper.find('[data-test-subj="no-enrichments-panel"]').hostNodes().text()).toContain( + i18n.NO_INDICATOR_ENRICHMENTS_TITLE + ); + }); + + it('renders expected text when investigation enrichments are absent', () => { + const wrapper = mount( + + ); + expect(wrapper.find('[data-test-subj="no-enrichments-panel"]').hostNodes().text()).toContain( + i18n.NO_INVESTIGATION_ENRICHMENTS_TITLE + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.tsx new file mode 100644 index 00000000000000..b521c3ba92c4d9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiHorizontalRule, EuiLink, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import { useKibana } from '../../../lib/kibana'; + +import * as i18n from './translations'; + +const Container = styled(EuiPanel)` + display: flex; + flex-direction: column; +`; + +const NoEnrichmentsPanelView: React.FC<{ + title: React.ReactNode; + description: React.ReactNode; +}> = ({ title, description }) => { + return ( + + {title} + + + {description} + + + ); +}; + +NoEnrichmentsPanelView.displayName = 'NoEnrichmentsPanelView'; + +export const NoEnrichmentsPanel: React.FC<{ + existingEnrichmentsCount: number; + investigationEnrichmentsCount: number; +}> = ({ existingEnrichmentsCount, investigationEnrichmentsCount }) => { + const threatIntelDocsUrl = `${ + useKibana().services.docLinks.links.filebeat.base + }/filebeat-module-threatintel.html`; + const noIndicatorEnrichmentsDescription = ( + <> + {i18n.IF_CTI_NOT_ENABLED} + + {i18n.CHECK_DOCS} + + + ); + + if (existingEnrichmentsCount === 0 && investigationEnrichmentsCount === 0) { + return ( + {i18n.NO_ENRICHMENTS_FOUND_TITLE}} + description={ + <> +

{noIndicatorEnrichmentsDescription}

+

{i18n.NO_INVESTIGATION_ENRICHMENTS_DESCRIPTION}

+ + } + /> + ); + } else if (existingEnrichmentsCount === 0) { + return ( + <> + + {i18n.NO_INDICATOR_ENRICHMENTS_TITLE}} + description={noIndicatorEnrichmentsDescription} + /> + + ); + } else if (investigationEnrichmentsCount === 0) { + return ( + <> + + {i18n.NO_INVESTIGATION_ENRICHMENTS_TITLE}} + description={i18n.NO_INVESTIGATION_ENRICHMENTS_DESCRIPTION} + /> + + ); + } else { + return null; + } +}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx index 0113dde96a4b6c..c25457a5e5e88d 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx @@ -31,15 +31,6 @@ describe('ThreatDetailsView', () => { ); }); - it('renders an empty view if there are no enrichments', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="empty-threat-details-view"]').exists()).toEqual(true); - }); - it('renders anchor links for event.url and event.reference', () => { const enrichments = [ buildEventEnrichmentMock({ diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx index d5e985c5757a62..b6b8a47c1dd8c5 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx @@ -20,7 +20,6 @@ import React, { Fragment } from 'react'; import { StyledEuiInMemoryTable } from '../summary_view'; import { getSummaryColumns, SummaryRow, ThreatDetailsRow } from '../helpers'; -import { EmptyThreatDetailsView } from './empty_threat_details_view'; import { FIRSTSEEN, EVENT_URL, EVENT_REFERENCE } from '../../../../../common/cti/constants'; import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; import { getFirstElement } from '../../../../../common/utils/data_retrieval'; @@ -70,15 +69,13 @@ const ThreatDetailsHeader: React.FC<{ + {isInvestigationTimeEnrichment(type) && ( + + + + )}
- {isInvestigationTimeEnrichment(type) && ( - - - - - - )} ); @@ -131,10 +128,6 @@ const buildThreatDetailsItems = (enrichment: CtiEnrichment) => const ThreatDetailsViewComponent: React.FC<{ enrichments: CtiEnrichment[]; }> = ({ enrichments }) => { - if (enrichments.length < 1) { - return ; - } - const sortedEnrichments = enrichments.sort((a, b) => getFirstSeen(b) - getFirstSeen(a)); return ( @@ -146,6 +139,7 @@ const ThreatDetailsViewComponent: React.FC<{ return ( + { ).toEqual('Summary'); }); }); + + describe('threat intel tab', () => { + it('renders a "no enrichments" panel view if there are no enrichments', () => { + alertsWrapper.find('[data-test-subj="threatIntelTab"]').first().simulate('click'); + expect(alertsWrapper.find('[data-test-subj="no-enrichments-panel"]').exists()).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 9afaaef61b17a4..7074212dcdb4c5 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -9,9 +9,6 @@ import { EuiTabbedContent, EuiTabbedContentTab, EuiSpacer, - EuiButton, - EuiFlexGroup, - EuiFlexItem, EuiLoadingContent, EuiLoadingSpinner, } from '@elastic/eui'; @@ -34,6 +31,7 @@ import { parseExistingEnrichments, timelineDataToEnrichment, } from './cti_details/helpers'; +import { NoEnrichmentsPanel } from './cti_details/no_enrichments_panel'; type EventViewTab = EuiTabbedContentTab; @@ -100,9 +98,6 @@ const EventDetailsComponent: React.FC = ({ (tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as EventViewId), [setSelectedTabId] ); - const viewThreatIntelTab = useCallback(() => setSelectedTabId(EventsViewType.threatIntelView), [ - setSelectedTabId, - ]); const eventFields = useMemo(() => getEnrichmentFields(data), [data]); const existingEnrichments = useMemo( @@ -118,6 +113,9 @@ const EventDetailsComponent: React.FC = ({ loading: enrichmentsLoading, result: enrichmentsResponse, } = useInvestigationTimeEnrichment(eventFields); + const investigationEnrichments = useMemo(() => enrichmentsResponse?.enrichments ?? [], [ + enrichmentsResponse?.enrichments, + ]); const allEnrichments = useMemo(() => { if (enrichmentsLoading || !enrichmentsResponse?.enrichments) { return existingEnrichments; @@ -140,29 +138,20 @@ const EventDetailsComponent: React.FC = ({ eventId: id, browserFields, timelineId, - title: i18n.ALERT_SUMMARY, }} /> + {enrichmentCount > 0 && ( + + )} {enrichmentsLoading && ( <> )} - {enrichmentCount > 0 && ( - <> - - - - - {i18n.VIEW_CTI_DATA} - - - - )} ), } @@ -176,7 +165,6 @@ const EventDetailsComponent: React.FC = ({ enrichmentsLoading, enrichmentCount, allEnrichments, - viewThreatIntelTab, ] ); @@ -192,10 +180,25 @@ const EventDetailsComponent: React.FC = ({ {enrichmentsLoading ? : `(${enrichmentCount})`} ), - content: , + content: ( + <> + + + + ), } : undefined, - [allEnrichments, enrichmentCount, enrichmentsLoading, isAlert] + [ + allEnrichments, + enrichmentCount, + enrichmentsLoading, + existingEnrichments.length, + investigationEnrichments.length, + isAlert, + ] ); const tableTab = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx index 0e846f3f6f6998..961860ed6d8b99 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx @@ -13,12 +13,13 @@ import { SummaryRow } from './helpers'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` - .euiTableHeaderCell { - border: none; - } + .euiTableHeaderCell, .euiTableRowCell { border: none; } + .euiTableHeaderCell .euiTableCellContent { + padding: 0; + } `; const StyledEuiTitle = styled(EuiTitle)` diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index a17ca5e434ace7..c632f5d6332e0e 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -11,22 +11,10 @@ export const SUMMARY = i18n.translate('xpack.securitySolution.alertDetails.summa defaultMessage: 'Summary', }); -export const ALERT_SUMMARY = i18n.translate('xpack.securitySolution.alertDetails.alertSummary', { - defaultMessage: 'Alert Summary', -}); - export const THREAT_INTEL = i18n.translate('xpack.securitySolution.alertDetails.threatIntel', { defaultMessage: 'Threat Intel', }); -export const THREAT_SUMMARY = i18n.translate('xpack.securitySolution.alertDetails.threatSummary', { - defaultMessage: 'Threat Summary', -}); - -export const VIEW_CTI_DATA = i18n.translate('xpack.securitySolution.alertDetails.threatIntelCta', { - defaultMessage: 'View threat intel data', -}); - export const INVESTIGATION_GUIDE = i18n.translate( 'xpack.securitySolution.alertDetails.summary.investigationGuide', { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 387547295f0470..2b1088d8c11aea 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -18562,16 +18562,13 @@ "xpack.securitySolution.administration.os.linux": "Linux", "xpack.securitySolution.administration.os.macos": "Mac", "xpack.securitySolution.administration.os.windows": "Windows", - "xpack.securitySolution.alertDetails.alertSummary": "アラート概要", "xpack.securitySolution.alertDetails.checkDocs": "マニュアルをご確認ください。", "xpack.securitySolution.alertDetails.ifCtiNotEnabled": "脅威インテリジェンスソースを有効にしていない場合で、この機能について関心がある場合は、", - "xpack.securitySolution.alertDetails.noEnrichmentFound": "Threat Intel Enrichmentが見つかりません", "xpack.securitySolution.alertDetails.summary": "まとめ", "xpack.securitySolution.alertDetails.summary.investigationGuide": "調査ガイド", "xpack.securitySolution.alertDetails.summary.readLess": "表示を減らす", "xpack.securitySolution.alertDetails.summary.readMore": "続きを読む", "xpack.securitySolution.alertDetails.threatIntel": "Threat Intel", - "xpack.securitySolution.alertDetails.threatSummary": "脅威概要", "xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel": "このルールで生成されたすべてのアラートのリスクスコアを選択します。", "xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle": "デフォルトリスクスコア", "xpack.securitySolution.alerts.riskScoreMapping.mappingDescriptionLabel": "ソースイベント値を使用して、デフォルトリスクスコアを上書きします。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d142969022c81a..04394a1ac17047 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -18826,16 +18826,13 @@ "xpack.securitySolution.administration.os.linux": "Linux", "xpack.securitySolution.administration.os.macos": "Mac", "xpack.securitySolution.administration.os.windows": "Windows", - "xpack.securitySolution.alertDetails.alertSummary": "告警摘要", "xpack.securitySolution.alertDetails.checkDocs": "请查看我们的文档。", "xpack.securitySolution.alertDetails.ifCtiNotEnabled": "如果尚未启用任何威胁情报来源,并希望更多了解此功能,", - "xpack.securitySolution.alertDetails.noEnrichmentFound": "未找到威胁情报扩充", "xpack.securitySolution.alertDetails.summary": "摘要", "xpack.securitySolution.alertDetails.summary.investigationGuide": "调查指南", "xpack.securitySolution.alertDetails.summary.readLess": "阅读更少内容", "xpack.securitySolution.alertDetails.summary.readMore": "阅读更多内容", "xpack.securitySolution.alertDetails.threatIntel": "威胁情报", - "xpack.securitySolution.alertDetails.threatSummary": "威胁摘要", "xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel": "选择此规则生成的所有告警的风险分数。", "xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle": "默认风险分数", "xpack.securitySolution.alerts.riskScoreMapping.mappingDescriptionLabel": "使用源事件值覆盖默认风险分数。", From c07f51e5be9b16c58a4988491d8ae2ce021b4aba Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Fri, 9 Jul 2021 16:23:00 -0600 Subject: [PATCH 67/77] [Security Detections] Fixes ip on threshold preview button when selecting an ip data type such as source.ip (#105126) ## Summary See https://github.com/elastic/kibana/issues/100433 for details and test instructions. This is considered critical and a small fix for 7.14.0 has been requested. * Wrote Cypress test that exercises the bug * Fixed mutation in one part of the Cypress Test * Decided to remove the "missing" that we were telling users was "others" since missing is not the same as others. It no longer errors, but some users might be asking why we don't show "others" anymore. The reality is that we only showed "missing" which isn't adding value to the preview of what detections will end up looking like. * Later if we want a true "others" we should implement it as a larger feature request and not a bug fix IMHO Before you would get errors in your network panel: ![errors_threshold](https://user-images.githubusercontent.com/1151048/125126681-b0380e00-e0b8-11eb-9f2c-a75e2909754c.png) After you now get the `source.ip` without errors: Screen Shot 2021-07-09 at 1 28 24 PM ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../detection_rules/threshold_rule.spec.ts | 27 ++++++++++++++++--- .../cypress/tasks/create_new_rule.ts | 2 +- .../components/rules/query_preview/index.tsx | 1 + 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index ad71d54eb2a7ad..ce00c9b40aead9 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -6,7 +6,7 @@ */ import { formatMitreAttackDescription } from '../../helpers/rules'; -import { indexPatterns, newRule, newThresholdRule } from '../../objects/rule'; +import { indexPatterns, newRule, newThresholdRule, ThresholdRule } from '../../objects/rule'; import { ALERT_RULE_METHOD, @@ -180,9 +180,9 @@ describe('Detection rules, threshold', () => { cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', rule.riskScore); }); - it('Preview results', () => { - const previewRule = { ...newThresholdRule }; - previewRule.index!.push('.siem-signals*'); + it('Preview results of keyword using "host.name"', () => { + const previewRule: ThresholdRule = { ...newThresholdRule }; + previewRule.index = [...previewRule.index, '.siem-signals*']; createCustomRuleActivated(newRule); goToManageAlertsDetectionRules(); @@ -194,4 +194,23 @@ describe('Detection rules, threshold', () => { cy.get(PREVIEW_HEADER_SUBTITLE).should('have.text', '3 unique hits'); }); + + it('Preview results of "ip" using "source.ip"', () => { + const previewRule: ThresholdRule = { + ...newThresholdRule, + thresholdField: 'source.ip', + threshold: '1', + }; + previewRule.index = [...previewRule.index, '.siem-signals*']; + + createCustomRuleActivated(newRule); + goToManageAlertsDetectionRules(); + waitForRulesTableToBeLoaded(); + goToCreateNewRule(); + selectThresholdRuleType(); + fillDefineThresholdRule(previewRule); + previewResults(); + + cy.get(PREVIEW_HEADER_SUBTITLE).should('have.text', '10 unique hits'); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 9b74110f0ef77d..1b420cd6d1520d 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -275,7 +275,7 @@ export const fillDefineThresholdRule = (rule: ThresholdRule) => { cy.get(TIMELINE(rule.timeline.id!)).click(); cy.get(COMBO_BOX_CLEAR_BTN).click(); - rule.index!.forEach((index) => { + rule.index.forEach((index) => { cy.get(COMBO_BOX_INPUT).first().type(`${index}{enter}`); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx index 6342d468f5962c..45b66058a04fb8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx @@ -118,6 +118,7 @@ export const PreviewQuery = ({ startDate: toTime, filterQuery: queryFilter, indexNames: index, + includeMissingData: false, histogramType: MatrixHistogramType.events, stackByField: 'event.category', threshold: ruleType === 'threshold' ? threshold : undefined, From b40fc09dfca3c2e7072ba33f65c9c4ee7474764c Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 9 Jul 2021 16:40:13 -0700 Subject: [PATCH 68/77] skip another suite blocking es promotion (#104466) --- test/functional/apps/discover/_large_string.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/_large_string.ts b/test/functional/apps/discover/_large_string.ts index de3f0f2c40ae10..ea219881c7a95a 100644 --- a/test/functional/apps/discover/_large_string.ts +++ b/test/functional/apps/discover/_large_string.ts @@ -19,7 +19,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover']); - describe('test large strings', function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104466 + describe.skip('test large strings', function () { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_large_strings']); From d776c0940ee47098bef21a129a03445e5fbf3eda Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 9 Jul 2021 18:01:11 -0700 Subject: [PATCH 69/77] skip all discover functional tests to unblock es promotion (#104466) --- test/functional/apps/discover/_data_grid_field_data.ts | 3 +-- test/functional/apps/discover/_field_data.ts | 3 +-- test/functional/apps/discover/_field_data_with_fields_api.ts | 3 +-- test/functional/apps/discover/_large_string.ts | 3 +-- test/functional/apps/discover/index.ts | 3 ++- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/test/functional/apps/discover/_data_grid_field_data.ts b/test/functional/apps/discover/_data_grid_field_data.ts index cf8a6fc1bd60f2..94e8e942f86ba2 100644 --- a/test/functional/apps/discover/_data_grid_field_data.ts +++ b/test/functional/apps/discover/_data_grid_field_data.ts @@ -19,8 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; const dataGrid = getService('dataGrid'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104466 - describe.skip('discover data grid field data tests', function describeIndexTests() { + describe('discover data grid field data tests', function describeIndexTests() { this.tags('includeFirefox'); before(async function () { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); diff --git a/test/functional/apps/discover/_field_data.ts b/test/functional/apps/discover/_field_data.ts index 6471b751945a80..ec9f9cf65e0fa8 100644 --- a/test/functional/apps/discover/_field_data.ts +++ b/test/functional/apps/discover/_field_data.ts @@ -20,8 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); const find = getService('find'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104466 - describe.skip('discover tab', function describeIndexTests() { + describe('discover tab', function describeIndexTests() { this.tags('includeFirefox'); before(async function () { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); diff --git a/test/functional/apps/discover/_field_data_with_fields_api.ts b/test/functional/apps/discover/_field_data_with_fields_api.ts index 7c6867e9350634..110e255d18c756 100644 --- a/test/functional/apps/discover/_field_data_with_fields_api.ts +++ b/test/functional/apps/discover/_field_data_with_fields_api.ts @@ -20,8 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); const find = getService('find'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104466 - describe.skip('discover tab with new fields API', function describeIndexTests() { + describe('discover tab with new fields API', function describeIndexTests() { this.tags('includeFirefox'); before(async function () { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); diff --git a/test/functional/apps/discover/_large_string.ts b/test/functional/apps/discover/_large_string.ts index ea219881c7a95a..de3f0f2c40ae10 100644 --- a/test/functional/apps/discover/_large_string.ts +++ b/test/functional/apps/discover/_large_string.ts @@ -19,8 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover']); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104466 - describe.skip('test large strings', function () { + describe('test large strings', function () { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_large_strings']); diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index b396f172f69612..a17bf53e7f4781 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -12,7 +12,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); - describe('discover app', function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104466 + describe.skip('discover app', function () { this.tags('ciGroup6'); before(function () { From c0fec48104a7d026802673723ffb8796211bc2e0 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Fri, 9 Jul 2021 20:55:20 -0600 Subject: [PATCH 70/77] [Security Solutions][Detection Engine] Fixes button group alignments in machine learning and tags (#105166) ## Summary See: https://github.com/elastic/kibana/issues/104055 For more issue details. This is deemed embarrassing enough to be critical for a fix for 7.14.0 before shipping. EUI looks to have updated its self and added a new attribute that it wants us to use called `numFIlters` which when set will show the total number of filter items before they are selected. Once selected the number and look and feel change. ```ts numFilters ``` Before: Screen Shot 2021-07-09 at 5 45 08 PM After before selections: Screen Shot 2021-07-09 at 5 48 43 PM After once you have selections: Screen Shot 2021-07-09 at 5 49 44 PM Before: Screen Shot 2021-07-09 at 5 42 01 PM After before selections: Screen Shot 2021-07-09 at 5 42 27 PM After once you have selections: Screen Shot 2021-07-09 at 5 49 36 PM ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../filters/__snapshots__/groups_filter_popover.test.tsx.snap | 1 + .../ml_popover/jobs_table/filters/groups_filter_popover.tsx | 1 + .../rules/all/rules_table_filters/tags_filter_popover.tsx | 2 ++ 3 files changed, 4 insertions(+) diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap index 22805d34d2ee1e..410fb7f3ae793e 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap @@ -10,6 +10,7 @@ exports[`GroupsFilterPopover renders correctly against snapshot 1`] = ` iconType="arrowDown" isSelected={false} numActiveFilters={0} + numFilters={3} onClick={[Function]} > Groups diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx index b7425a62f6773b..249dc0dfccdbb6 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx @@ -59,6 +59,7 @@ export const GroupsFilterPopoverComponent = ({ iconType="arrowDown" onClick={() => setIsGroupPopoverOpen(!isGroupPopoverOpen)} isSelected={isGroupPopoverOpen} + numFilters={uniqueGroups.length} hasActiveFilters={selectedGroups.length > 0} numActiveFilters={selectedGroups.length} > diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx index 45ce5bc18361c9..c5262caf6c776c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx @@ -102,9 +102,11 @@ const TagsFilterPopoverComponent = ({ ownFocus button={ setIsTagPopoverOpen(!isTagPopoverOpen)} + numFilters={tags.length} isSelected={isTagPopoverOpen} hasActiveFilters={selectedTags.length > 0} numActiveFilters={selectedTags.length} From 857dc9f2e1e1edd86c9cf7cd5f157e3e8b82a14a Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Sat, 10 Jul 2021 09:27:07 +0200 Subject: [PATCH 71/77] [APM] Don't log error if request was aborted (#105024) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../routes/register_routes/index.test.ts | 23 ++++-- .../server/routes/register_routes/index.ts | 72 ++++++++++++------- 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.test.ts b/x-pack/plugins/apm/server/routes/register_routes/index.test.ts index 158d7ee7e76a3f..b9dece866fae55 100644 --- a/x-pack/plugins/apm/server/routes/register_routes/index.test.ts +++ b/x-pack/plugins/apm/server/routes/register_routes/index.test.ts @@ -109,6 +109,11 @@ const initApi = ( params: {}, query: {}, body: null, + events: { + aborted$: { + toPromise: () => new Promise(() => {}), + }, + }, ...request, }, responseMock @@ -202,7 +207,7 @@ describe('createApi', () => { describe('when validating', () => { describe('_inspect', () => { it('allows _inspect=true', async () => { - const handlerMock = jest.fn(); + const handlerMock = jest.fn().mockResolvedValue({}); const { simulateRequest, mocks: { response }, @@ -234,7 +239,7 @@ describe('createApi', () => { }); it('rejects _inspect=1', async () => { - const handlerMock = jest.fn(); + const handlerMock = jest.fn().mockResolvedValue({}); const { simulateRequest, @@ -267,7 +272,7 @@ describe('createApi', () => { }); it('allows omitting _inspect', async () => { - const handlerMock = jest.fn(); + const handlerMock = jest.fn().mockResolvedValue({}); const { simulateRequest, @@ -297,7 +302,11 @@ describe('createApi', () => { simulateRequest, mocks: { response }, } = initApi([ - { endpoint: 'GET /foo', options: { tags: [] }, handler: jest.fn() }, + { + endpoint: 'GET /foo', + options: { tags: [] }, + handler: jest.fn().mockResolvedValue({}), + }, ]); await simulateRequest({ @@ -328,7 +337,7 @@ describe('createApi', () => { }); it('validates path parameters', async () => { - const handlerMock = jest.fn(); + const handlerMock = jest.fn().mockResolvedValue({}); const { simulateRequest, mocks: { response }, @@ -402,7 +411,7 @@ describe('createApi', () => { }); it('validates body parameters', async () => { - const handlerMock = jest.fn(); + const handlerMock = jest.fn().mockResolvedValue({}); const { simulateRequest, mocks: { response }, @@ -448,7 +457,7 @@ describe('createApi', () => { }); it('validates query parameters', async () => { - const handlerMock = jest.fn(); + const handlerMock = jest.fn().mockResolvedValue({}); const { simulateRequest, mocks: { response }, diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.ts b/x-pack/plugins/apm/server/routes/register_routes/index.ts index 136f3c73d80469..8e6070de722bea 100644 --- a/x-pack/plugins/apm/server/routes/register_routes/index.ts +++ b/x-pack/plugins/apm/server/routes/register_routes/index.ts @@ -29,6 +29,13 @@ const inspectRt = t.exact( }) ); +const CLIENT_CLOSED_REQUEST = { + statusCode: 499, + body: { + message: 'Client closed request', + }, +}; + export const inspectableEsQueriesMap = new WeakMap< KibanaRequest, InspectResponse @@ -89,23 +96,40 @@ export function registerRoutes({ runtimeType ); - const data: Record | undefined | null = (await handler({ - request, - context, - config, - logger, - core, - plugins, - params: merge( - { - query: { - _inspect: false, + const { aborted, data } = await Promise.race([ + handler({ + request, + context, + config, + logger, + core, + plugins, + params: merge( + { + query: { + _inspect: false, + }, }, - }, - validatedParams - ), - ruleDataClient, - })) as any; + validatedParams + ), + ruleDataClient, + }).then((value) => { + return { + aborted: false, + data: value as Record | undefined | null, + }; + }), + request.events.aborted$.toPromise().then(() => { + return { + aborted: true, + data: undefined, + }; + }), + ]); + + if (aborted) { + return response.custom(CLIENT_CLOSED_REQUEST); + } if (Array.isArray(data)) { throw new Error('Return type cannot be an array'); @@ -118,9 +142,6 @@ export function registerRoutes({ } : { ...data }; - // cleanup - inspectableEsQueriesMap.delete(request); - if (!options.disableTelemetry && telemetryUsageCounter) { telemetryUsageCounter.incrementCounter({ counterName: `${method.toUpperCase()} ${pathname}`, @@ -131,6 +152,7 @@ export function registerRoutes({ return response.ok({ body }); } catch (error) { logger.error(error); + if (!options.disableTelemetry && telemetryUsageCounter) { telemetryUsageCounter.incrementCounter({ counterName: `${method.toUpperCase()} ${pathname}`, @@ -147,16 +169,18 @@ export function registerRoutes({ }, }; - if (Boom.isBoom(error)) { - opts.statusCode = error.output.statusCode; + if (error instanceof RequestAbortedError) { + return response.custom(merge(opts, CLIENT_CLOSED_REQUEST)); } - if (error instanceof RequestAbortedError) { - opts.statusCode = 499; - opts.body.message = 'Client closed request'; + if (Boom.isBoom(error)) { + opts.statusCode = error.output.statusCode; } return response.custom(opts); + } finally { + // cleanup + inspectableEsQueriesMap.delete(request); } } ); From e4ba52928c289547d97706b083bed1202be1176d Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Sat, 10 Jul 2021 18:34:03 -0500 Subject: [PATCH 72/77] [Security Solution] remove query strategy v1 (#104196) --- .../common/endpoint/types/index.ts | 14 - .../management/pages/endpoint_hosts/mocks.ts | 4 - .../pages/endpoint_hosts/store/builders.ts | 1 - .../pages/endpoint_hosts/store/middleware.ts | 3 +- .../store/mock_endpoint_result_list.ts | 16 +- .../pages/endpoint_hosts/store/reducer.ts | 2 - .../pages/endpoint_hosts/store/selectors.ts | 7 - .../management/pages/endpoint_hosts/types.ts | 3 - .../pages/endpoint_hosts/view/index.test.tsx | 34 +- .../pages/endpoint_hosts/view/index.tsx | 9 +- .../endpoint/endpoint_app_context_services.ts | 54 --- .../endpoint/routes/actions/isolation.ts | 2 +- .../routes/metadata/enrichment.test.ts | 86 +--- .../endpoint/routes/metadata/handlers.ts | 85 +--- .../server/endpoint/routes/metadata/index.ts | 27 +- .../endpoint/routes/metadata/metadata.test.ts | 107 +--- .../routes/metadata/metadata_v1.test.ts | 456 ------------------ .../routes/metadata/query_builders.test.ts | 73 ++- .../routes/metadata/query_builders.ts | 24 +- .../routes/metadata/query_builders_v1.test.ts | 188 -------- .../metadata/support/query_strategies.ts | 123 ++--- .../routes/metadata/support/test_support.ts | 56 --- .../server/endpoint/services/metadata.ts | 13 +- .../server/endpoint/types.ts | 15 +- .../factory/hosts/details/helpers.ts | 14 +- .../apis/index.ts | 1 - .../apis/metadata.ts | 9 - .../apis/metadata_v1.ts | 290 ----------- 28 files changed, 124 insertions(+), 1592 deletions(-) delete mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts delete mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts delete mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/metadata_v1.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 1e0d798cf7f07d..cf8c33d38f8fa9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -178,8 +178,6 @@ export interface HostResultList { request_page_size: number; /* the page index requested */ request_page_index: number; - /* the version of the query strategy */ - query_strategy_version: MetadataQueryStrategyVersions; /* policy IDs and versions */ policy_info?: HostInfo['policy_info']; } @@ -404,21 +402,11 @@ export enum HostStatus { INACTIVE = 'inactive', } -export enum MetadataQueryStrategyVersions { - VERSION_1 = 'v1', - VERSION_2 = 'v2', -} - export type PolicyInfo = Immutable<{ revision: number; id: string; }>; -export interface HostMetadataInfo { - metadata: HostMetadata; - query_strategy_version: MetadataQueryStrategyVersions; -} - export type HostInfo = Immutable<{ metadata: HostMetadata; host_status: HostStatus; @@ -438,8 +426,6 @@ export type HostInfo = Immutable<{ */ endpoint: PolicyInfo; }; - /* the version of the query strategy */ - query_strategy_version: MetadataQueryStrategyVersions; }>; // HostMetadataDetails is now just HostMetadata diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts index d6b24fa3cbdfc2..76de52222bbd39 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts @@ -15,7 +15,6 @@ import { HostPolicyResponse, HostResultList, HostStatus, - MetadataQueryStrategyVersions, } from '../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; import { @@ -54,7 +53,6 @@ export const endpointMetadataHttpMocks = httpHandlerMockFactory => { agentsWithEndpointsTotalError: undefined, endpointsTotal: 0, endpointsTotalError: undefined, - queryStrategyVersion: undefined, policyVersionInfo: undefined, hostStatus: undefined, isolationRequestState: createUninitialisedResourceState(), diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index f233fbdec5415e..922f10cee2f8b4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -28,7 +28,6 @@ import { nonExistingPolicies, patterns, searchBarQuery, - isTransformEnabled, getIsIsolationRequestPending, getCurrentIsolationRequestState, getActivityLogData, @@ -180,7 +179,7 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory HostResultList = (options = {}) => { const { total = 1, request_page_size: requestPageSize = 10, request_page_index: requestPageIndex = 0, - query_strategy_version: queryStrategyVersion = MetadataQueryStrategyVersions.VERSION_2, } = options; // Skip any that are before the page we're on @@ -58,7 +55,6 @@ export const mockEndpointResultList: (options?: { hosts.push({ metadata: generator.generateHostMetadata(), host_status: HostStatus.UNHEALTHY, - query_strategy_version: queryStrategyVersion, }); } const mock: HostResultList = { @@ -66,7 +62,6 @@ export const mockEndpointResultList: (options?: { total, request_page_size: requestPageSize, request_page_index: requestPageIndex, - query_strategy_version: queryStrategyVersion, }; return mock; }; @@ -78,7 +73,6 @@ export const mockEndpointDetailsApiResult = (): HostInfo => { return { metadata: generator.generateHostMetadata(), host_status: HostStatus.UNHEALTHY, - query_strategy_version: MetadataQueryStrategyVersions.VERSION_2, }; }; @@ -92,7 +86,6 @@ const endpointListApiPathHandlerMocks = ({ endpointPackagePolicies = [], policyResponse = generator.generatePolicyResponse(), agentPolicy = generator.generateAgentPolicy(), - queryStrategyVersion = MetadataQueryStrategyVersions.VERSION_2, totalAgentsUsingEndpoint = 0, }: { /** route handlers will be setup for each individual host in this array */ @@ -101,7 +94,6 @@ const endpointListApiPathHandlerMocks = ({ endpointPackagePolicies?: GetPolicyListResponse['items']; policyResponse?: HostPolicyResponse; agentPolicy?: GetAgentPoliciesResponseItem; - queryStrategyVersion?: MetadataQueryStrategyVersions; totalAgentsUsingEndpoint?: number; } = {}) => { const apiHandlers = { @@ -119,7 +111,6 @@ const endpointListApiPathHandlerMocks = ({ request_page_size: 10, request_page_index: 0, total: endpointsResults?.length || 0, - query_strategy_version: queryStrategyVersion, }; }, @@ -192,16 +183,11 @@ export const setEndpointListApiMockImplementation: ( apiResponses?: Parameters[0] ) => void = ( mockedHttpService, - { - endpointsResults = mockEndpointResultList({ total: 3 }).hosts, - queryStrategyVersion = MetadataQueryStrategyVersions.VERSION_2, - ...pathHandlersOptions - } = {} + { endpointsResults = mockEndpointResultList({ total: 3 }).hosts, ...pathHandlersOptions } = {} ) => { const apiHandlers = endpointListApiPathHandlerMocks({ ...pathHandlersOptions, endpointsResults, - queryStrategyVersion, }); mockedHttpService.post diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 1498ce08db8abc..c6bf13a3b5715d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -89,7 +89,6 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta total, request_page_size: pageSize, request_page_index: pageIndex, - query_strategy_version: queryStrategyVersion, policy_info: policyVersionInfo, } = action.payload; return { @@ -98,7 +97,6 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta total, pageSize, pageIndex, - queryStrategyVersion, policyVersionInfo, loading: false, error: undefined, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index 5771fbac957d8b..4287cf9a109ea3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -15,7 +15,6 @@ import { HostPolicyResponseAppliedAction, HostPolicyResponseConfiguration, HostPolicyResponseActionStatus, - MetadataQueryStrategyVersions, HostStatus, ActivityLog, HostMetadata, @@ -90,17 +89,11 @@ export const agentsWithEndpointsTotalError = (state: Immutable) = state.agentsWithEndpointsTotalError; export const endpointsTotalError = (state: Immutable) => state.endpointsTotalError; -const queryStrategyVersion = (state: Immutable) => state.queryStrategyVersion; export const endpointPackageVersion = createSelector(endpointPackageInfo, (info) => isLoadedResourceState(info) ? info.data.version : undefined ); -export const isTransformEnabled = createSelector( - queryStrategyVersion, - (version) => version !== MetadataQueryStrategyVersions.VERSION_1 -); - /** * Returns the index patterns for the SearchBar to use for autosuggest */ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index 144cc7a64d6cbc..875841cb55b738 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -13,7 +13,6 @@ import { HostPolicyResponse, AppLocation, PolicyData, - MetadataQueryStrategyVersions, HostStatus, HostIsolationResponse, EndpointPendingActions, @@ -96,8 +95,6 @@ export interface EndpointState { endpointsTotal: number; /** api error for total, actual Endpoints */ endpointsTotalError?: ServerApiError; - /** The query strategy version that informs whether the transform for KQL is enabled or not */ - queryStrategyVersion?: MetadataQueryStrategyVersions; /** The policy IDs and revision number of the corresponding agent, and endpoint. May be more recent than what's running */ policyVersionInfo?: HostInfo['policy_info']; /** The status of the host, which is mapped to the Elastic Agent status in Fleet */ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index aafac38accd89f..26d0d53e39982a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -23,7 +23,6 @@ import { HostPolicyResponseActionStatus, HostPolicyResponseAppliedAction, HostStatus, - MetadataQueryStrategyVersions, } from '../../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; import { POLICY_STATUS_TO_TEXT } from './host_constants'; @@ -167,31 +166,6 @@ describe('when on the endpoint list page', () => { }); }); - describe('when loading data with the query_strategy_version is `v1`', () => { - beforeEach(() => { - reactTestingLibrary.act(() => { - const mockedEndpointListData = mockEndpointResultList({ - total: 4, - query_strategy_version: MetadataQueryStrategyVersions.VERSION_1, - }); - setEndpointListApiMockImplementation(coreStart.http, { - endpointsResults: mockedEndpointListData.hosts, - queryStrategyVersion: mockedEndpointListData.query_strategy_version, - }); - }); - }); - afterEach(() => { - jest.clearAllMocks(); - }); - it('should not display the KQL bar', async () => { - const renderResult = render(); - await reactTestingLibrary.act(async () => { - await middlewareSpy.waitForAction('serverReturnedEndpointList'); - }); - expect(renderResult.queryByTestId('adminSearchBar')).toBeNull(); - }); - }); - describe('when determining when to show the enrolling message', () => { afterEach(() => { jest.clearAllMocks(); @@ -268,7 +242,6 @@ describe('when on the endpoint list page', () => { reactTestingLibrary.act(() => { const mockedEndpointData = mockEndpointResultList({ total: 5 }); const hostListData = mockedEndpointData.hosts; - const queryStrategyVersion = mockedEndpointData.query_strategy_version; firstPolicyID = hostListData[0].metadata.Endpoint.policy.applied.id; firstPolicyRev = hostListData[0].metadata.Endpoint.policy.applied.endpoint_policy_version; @@ -329,7 +302,6 @@ describe('when on the endpoint list page', () => { hostListData[index].metadata.Endpoint.policy.applied, setup.policy ), - query_strategy_version: queryStrategyVersion, }; }); hostListData.forEach((item, index) => { @@ -535,8 +507,6 @@ describe('when on the endpoint list page', () => { // eslint-disable-next-line @typescript-eslint/naming-convention host_status, metadata: { agent, Endpoint, ...details }, - // eslint-disable-next-line @typescript-eslint/naming-convention - query_strategy_version, } = mockEndpointDetailsApiResult(); hostDetails = { @@ -555,7 +525,6 @@ describe('when on the endpoint list page', () => { id: '1', }, }, - query_strategy_version, }; const policy = docGenerator.generatePolicyPackagePolicy(); @@ -1198,7 +1167,7 @@ describe('when on the endpoint list page', () => { let renderResult: ReturnType; const mockEndpointListApi = () => { - const { hosts, query_strategy_version: queryStrategyVersion } = mockEndpointResultList(); + const { hosts } = mockEndpointResultList(); hostInfo = { host_status: hosts[0].host_status, metadata: { @@ -1222,7 +1191,6 @@ describe('when on the endpoint list page', () => { version: '7.14.0', }, }, - query_strategy_version: queryStrategyVersion, }; const packagePolicy = docGenerator.generatePolicyPackagePolicy(); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 0ee345431055bb..c78d4ca6af634b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -120,7 +120,6 @@ export const EndpointList = () => { areEndpointsEnrolling, agentsWithEndpointsTotalError, endpointsTotalError, - isTransformEnabled, } = useEndpointSelector(selector); const { search } = useFormatUrl(SecurityPageName.administration); const { getAppUrl } = useAppUrl(); @@ -476,8 +475,8 @@ export const EndpointList = () => { const hasListData = listData && listData.length > 0; const refreshStyle = useMemo(() => { - return { display: endpointsExist && isTransformEnabled ? 'flex' : 'none', maxWidth: 200 }; - }, [endpointsExist, isTransformEnabled]); + return { display: endpointsExist ? 'flex' : 'none', maxWidth: 200 }; + }, [endpointsExist]); const refreshIsPaused = useMemo(() => { return !endpointsExist ? false : hasSelectedEndpoint ? true : !isAutoRefreshEnabled; @@ -492,8 +491,8 @@ export const EndpointList = () => { }, [endpointsTotalError, agentsWithEndpointsTotalError]); const shouldShowKQLBar = useMemo(() => { - return endpointsExist && !patternsError && isTransformEnabled; - }, [endpointsExist, patternsError, isTransformEnabled]); + return endpointsExist && !patternsError; + }, [endpointsExist, patternsError]); return ( ; -} - -export const createMetadataService = (packageService: PackageService): MetadataService => { - return { - async queryStrategy( - savedObjectsClient: SavedObjectsClientContract, - version?: MetadataQueryStrategyVersions - ): Promise { - if (version === MetadataQueryStrategyVersions.VERSION_1) { - return metadataQueryStrategyV1(); - } - if (!packageService) { - throw new Error('package service is uninitialized'); - } - - if (version === MetadataQueryStrategyVersions.VERSION_2 || !version) { - const assets = - (await packageService.getInstallation({ savedObjectsClient, pkgName: 'endpoint' })) - ?.installed_es ?? []; - const expectedTransformAssets = assets.filter( - (ref) => - ref.type === ElasticsearchAssetType.transform && - ref.id.startsWith(metadataTransformPrefix) - ); - if (expectedTransformAssets && expectedTransformAssets.length === 1) { - return metadataQueryStrategyV2(); - } - return metadataQueryStrategyV1(); - } - return metadataQueryStrategyV1(); - }, - }; -}; - export type EndpointAppContextServiceStartContract = Partial< Pick< FleetStartContract, @@ -114,7 +66,6 @@ export class EndpointAppContextService { private packagePolicyService: PackagePolicyServiceInterface | undefined; private agentPolicyService: AgentPolicyServiceInterface | undefined; private savedObjectsStart: SavedObjectsServiceStart | undefined; - private metadataService: MetadataService | undefined; private config: ConfigType | undefined; private license: LicenseService | undefined; public security: SecurityPluginStart | undefined; @@ -128,7 +79,6 @@ export class EndpointAppContextService { this.agentPolicyService = dependencies.agentPolicyService; this.manifestManager = dependencies.manifestManager; this.savedObjectsStart = dependencies.savedObjectsStart; - this.metadataService = createMetadataService(dependencies.packageService!); this.config = dependencies.config; this.license = dependencies.licenseService; this.security = dependencies.security; @@ -176,10 +126,6 @@ export class EndpointAppContextService { return this.agentPolicyService; } - public getMetadataService(): MetadataService | undefined { - return this.metadataService; - } - public getManifestManager(): ManifestManager | undefined { return this.manifestManager; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts index 45063ca92e2b0a..fceb45b17c2587 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts @@ -83,7 +83,7 @@ export const isolationRequestHandler = function ( // fetch the Agent IDs to send the commands to const endpointIDs = [...new Set(req.body.endpoint_ids)]; // dedupe - const endpointData = await getMetadataForEndpoints(endpointIDs, context, endpointContext); + const endpointData = await getMetadataForEndpoints(endpointIDs, context); const casesClient = await endpointContext.service.getCasesClient(req); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/enrichment.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/enrichment.test.ts index 960f3abda81956..39aa0bf2d8cf77 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/enrichment.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/enrichment.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { HostStatus, MetadataQueryStrategyVersions } from '../../../../common/endpoint/types'; +import { HostStatus } from '../../../../common/endpoint/types'; import { createMockMetadataRequestContext } from '../../mocks'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; import { enrichHostMetadata, MetadataRequestContext } from './handlers'; @@ -18,30 +18,6 @@ describe('test document enrichment', () => { metaReqCtx = createMockMetadataRequestContext(); }); - // verify query version passed through - describe('metadata query strategy enrichment', () => { - it('should match v1 strategy when directed', async () => { - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_1 - ); - expect(enrichedHostList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_1 - ); - }); - it('should match v2 strategy when directed', async () => { - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); - expect(enrichedHostList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_2 - ); - }); - }); - describe('host status enrichment', () => { let statusFn: jest.Mock; @@ -57,77 +33,49 @@ describe('test document enrichment', () => { it('should return host healthy for online agent', async () => { statusFn.mockImplementation(() => 'online'); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.host_status).toEqual(HostStatus.HEALTHY); }); it('should return host offline for offline agent', async () => { statusFn.mockImplementation(() => 'offline'); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.host_status).toEqual(HostStatus.OFFLINE); }); it('should return host updating for unenrolling agent', async () => { statusFn.mockImplementation(() => 'unenrolling'); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.host_status).toEqual(HostStatus.UPDATING); }); it('should return host unhealthy for degraded agent', async () => { statusFn.mockImplementation(() => 'degraded'); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.host_status).toEqual(HostStatus.UNHEALTHY); }); it('should return host unhealthy for erroring agent', async () => { statusFn.mockImplementation(() => 'error'); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.host_status).toEqual(HostStatus.UNHEALTHY); }); it('should return host unhealthy for warning agent', async () => { statusFn.mockImplementation(() => 'warning'); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.host_status).toEqual(HostStatus.UNHEALTHY); }); it('should return host unhealthy for invalid agent', async () => { statusFn.mockImplementation(() => 'asliduasofb'); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.host_status).toEqual(HostStatus.UNHEALTHY); }); }); @@ -164,11 +112,7 @@ describe('test document enrichment', () => { }; }); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.policy_info).toBeDefined(); expect(enrichedHostList.policy_info!.agent.applied.id).toEqual(policyID); expect(enrichedHostList.policy_info!.agent.applied.revision).toEqual(policyRev); @@ -184,11 +128,7 @@ describe('test document enrichment', () => { }; }); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.policy_info).toBeDefined(); expect(enrichedHostList.policy_info!.agent.configured.id).toEqual(policyID); expect(enrichedHostList.policy_info!.agent.configured.revision).toEqual(policyRev); @@ -209,11 +149,7 @@ describe('test document enrichment', () => { }; }); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.policy_info).toBeDefined(); expect(enrichedHostList.policy_info!.endpoint.id).toEqual(policyID); expect(enrichedHostList.policy_info!.endpoint.revision).toEqual(policyRev); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index 815f30e6e7426f..2ceca170881e32 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -17,10 +17,8 @@ import { import { HostInfo, HostMetadata, - HostMetadataInfo, HostResultList, HostStatus, - MetadataQueryStrategyVersions, } from '../../../../common/endpoint/types'; import type { SecuritySolutionRequestHandlerContext } from '../../../types'; @@ -33,6 +31,10 @@ import { findAllUnenrolledAgentIds } from './support/unenroll'; import { findAgentIDsByStatus } from './support/agent_status'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { fleetAgentStatusToEndpointHostStatus } from '../../utils'; +import { + queryResponseToHostListResult, + queryResponseToHostResult, +} from './support/query_strategies'; export interface MetadataRequestContext { esClient?: IScopedClusterClient; @@ -58,8 +60,7 @@ export const getLogger = (endpointAppContext: EndpointAppContext): Logger => { export const getMetadataListRequestHandler = function ( endpointAppContext: EndpointAppContext, - logger: Logger, - queryStrategyVersion?: MetadataQueryStrategyVersions + logger: Logger ): RequestHandler< unknown, unknown, @@ -96,24 +97,15 @@ export const getMetadataListRequestHandler = function ( ) : undefined; - const queryStrategy = await endpointAppContext.service - ?.getMetadataService() - ?.queryStrategy(context.core.savedObjects.client, queryStrategyVersion); - - const queryParams = await kibanaRequestToMetadataListESQuery( - request, - endpointAppContext, - queryStrategy!, - { - unenrolledAgentIds: unenrolledAgentIds.concat(IGNORED_ELASTIC_AGENT_IDS), - statusAgentIDs: statusIDs, - } - ); + const queryParams = await kibanaRequestToMetadataListESQuery(request, endpointAppContext, { + unenrolledAgentIds: unenrolledAgentIds.concat(IGNORED_ELASTIC_AGENT_IDS), + statusAgentIDs: statusIDs, + }); const result = await context.core.elasticsearch.client.asCurrentUser.search( queryParams ); - const hostListQueryResult = queryStrategy!.queryResponseToHostListResult(result.body); + const hostListQueryResult = queryResponseToHostListResult(result.body); return response.ok({ body: await mapToHostResultList(queryParams, hostListQueryResult, metadataRequestContext), }); @@ -122,8 +114,7 @@ export const getMetadataListRequestHandler = function ( export const getMetadataRequestHandler = function ( endpointAppContext: EndpointAppContext, - logger: Logger, - queryStrategyVersion?: MetadataQueryStrategyVersions + logger: Logger ): RequestHandler< TypeOf, unknown, @@ -145,11 +136,7 @@ export const getMetadataRequestHandler = function ( }; try { - const doc = await getHostData( - metadataRequestContext, - request?.params?.id, - queryStrategyVersion - ); + const doc = await getHostData(metadataRequestContext, request?.params?.id); if (doc) { return response.ok({ body: doc }); } @@ -169,9 +156,8 @@ export const getMetadataRequestHandler = function ( export async function getHostMetaData( metadataRequestContext: MetadataRequestContext, - id: string, - queryStrategyVersion?: MetadataQueryStrategyVersions -): Promise { + id: string +): Promise { if ( !metadataRequestContext.esClient && !metadataRequestContext.requestHandlerContext?.core.elasticsearch.client @@ -190,32 +176,23 @@ export async function getHostMetaData( metadataRequestContext.requestHandlerContext?.core.elasticsearch .client) as IScopedClusterClient; - const esSavedObjectClient = - metadataRequestContext?.savedObjectsClient ?? - (metadataRequestContext.requestHandlerContext?.core.savedObjects - .client as SavedObjectsClientContract); - - const queryStrategy = await metadataRequestContext.endpointAppContextService - ?.getMetadataService() - ?.queryStrategy(esSavedObjectClient, queryStrategyVersion); - const query = getESQueryHostMetadataByID(id, queryStrategy!); + const query = getESQueryHostMetadataByID(id); const response = await esClient.asCurrentUser.search(query); - const hostResult = queryStrategy!.queryResponseToHostResult(response.body); + const hostResult = queryResponseToHostResult(response.body); const hostMetadata = hostResult.result; if (!hostMetadata) { return undefined; } - return { metadata: hostMetadata, query_strategy_version: hostResult.queryStrategyVersion }; + return hostMetadata; } export async function getHostData( metadataRequestContext: MetadataRequestContext, - id: string, - queryStrategyVersion?: MetadataQueryStrategyVersions + id: string ): Promise { if (!metadataRequestContext.savedObjectsClient) { throw Boom.badRequest('savedObjectsClient not found'); @@ -228,25 +205,21 @@ export async function getHostData( throw Boom.badRequest('esClient not found'); } - const hostResult = await getHostMetaData(metadataRequestContext, id, queryStrategyVersion); + const hostMetadata = await getHostMetaData(metadataRequestContext, id); - if (!hostResult) { + if (!hostMetadata) { return undefined; } - const agent = await findAgent(metadataRequestContext, hostResult.metadata); + const agent = await findAgent(metadataRequestContext, hostMetadata); if (agent && !agent.active) { throw Boom.badRequest('the requested endpoint is unenrolled'); } - const metadata = await enrichHostMetadata( - hostResult.metadata, - metadataRequestContext, - hostResult.query_strategy_version - ); + const metadata = await enrichHostMetadata(hostMetadata, metadataRequestContext); - return { ...metadata, query_strategy_version: hostResult.query_strategy_version }; + return metadata; } async function findAgent( @@ -293,15 +266,10 @@ export async function mapToHostResultList( request_page_index: queryParams.from, hosts: await Promise.all( hostListQueryResult.resultList.map(async (entry) => - enrichHostMetadata( - entry, - metadataRequestContext, - hostListQueryResult.queryStrategyVersion - ) + enrichHostMetadata(entry, metadataRequestContext) ) ), total: totalNumberOfHosts, - query_strategy_version: hostListQueryResult.queryStrategyVersion, }; } else { return { @@ -309,15 +277,13 @@ export async function mapToHostResultList( request_page_index: queryParams.from, total: totalNumberOfHosts, hosts: [], - query_strategy_version: hostListQueryResult.queryStrategyVersion, }; } } export async function enrichHostMetadata( hostMetadata: HostMetadata, - metadataRequestContext: MetadataRequestContext, - metadataQueryStrategyVersion: MetadataQueryStrategyVersions + metadataRequestContext: MetadataRequestContext ): Promise { let hostStatus = HostStatus.UNHEALTHY; let elasticAgentId = hostMetadata?.elastic?.agent?.id; @@ -413,6 +379,5 @@ export async function enrichHostMetadata( metadata: hostMetadata, host_status: hostStatus, policy_info: policyInfo, - query_strategy_version: metadataQueryStrategyVersion, }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index b4784c1ff5ed40..d9c3e6c195307d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -7,19 +7,15 @@ import { schema } from '@kbn/config-schema'; -import { HostStatus, MetadataQueryStrategyVersions } from '../../../../common/endpoint/types'; +import { HostStatus } from '../../../../common/endpoint/types'; import { EndpointAppContext } from '../../types'; import { getLogger, getMetadataListRequestHandler, getMetadataRequestHandler } from './handlers'; import type { SecuritySolutionPluginRouter } from '../../../types'; import { - BASE_ENDPOINT_ROUTE, HOST_METADATA_GET_ROUTE, HOST_METADATA_LIST_ROUTE, } from '../../../../common/endpoint/constants'; -export const METADATA_REQUEST_V1_ROUTE = `${BASE_ENDPOINT_ROUTE}/v1/metadata`; -export const GET_METADATA_REQUEST_V1_ROUTE = `${METADATA_REQUEST_V1_ROUTE}/{id}`; - /* Filters that can be applied to the endpoint fetch route */ export const endpointFilters = schema.object({ kql: schema.nullable(schema.string()), @@ -69,18 +65,6 @@ export function registerEndpointRoutes( endpointAppContext: EndpointAppContext ) { const logger = getLogger(endpointAppContext); - router.post( - { - path: `${METADATA_REQUEST_V1_ROUTE}`, - validate: GetMetadataListRequestSchema, - options: { authRequired: true, tags: ['access:securitySolution'] }, - }, - getMetadataListRequestHandler( - endpointAppContext, - logger, - MetadataQueryStrategyVersions.VERSION_1 - ) - ); router.post( { @@ -91,15 +75,6 @@ export function registerEndpointRoutes( getMetadataListRequestHandler(endpointAppContext, logger) ); - router.get( - { - path: `${GET_METADATA_REQUEST_V1_ROUTE}`, - validate: GetMetadataRequestSchema, - options: { authRequired: true, tags: ['access:securitySolution'] }, - }, - getMetadataRequestHandler(endpointAppContext, logger, MetadataQueryStrategyVersions.VERSION_1) - ); - router.get( { path: `${HOST_METADATA_GET_ROUTE}`, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index 5250f7c49d6ade..1e56f79aa0b324 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -19,12 +19,7 @@ import { loggingSystemMock, savedObjectsClientMock, } from '../../../../../../../src/core/server/mocks'; -import { - HostInfo, - HostResultList, - HostStatus, - MetadataQueryStrategyVersions, -} from '../../../../common/endpoint/types'; +import { HostInfo, HostResultList, HostStatus } from '../../../../common/endpoint/types'; import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; import { registerEndpointRoutes } from './index'; import { @@ -39,7 +34,7 @@ import { import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; import { Agent, ElasticsearchAssetType } from '../../../../../fleet/common/types/models'; -import { createV1SearchResponse, createV2SearchResponse } from './support/test_support'; +import { createV2SearchResponse } from './support/test_support'; import { PackageService } from '../../../../../fleet/server/services'; import { HOST_METADATA_LIST_ROUTE, @@ -98,94 +93,6 @@ describe('test endpoint route', () => { ); }); - describe('with no transform package', () => { - beforeEach(() => { - endpointAppContextService = new EndpointAppContextService(); - mockPackageService = createMockPackageService(); - mockPackageService.getInstallation.mockReturnValue(Promise.resolve(undefined)); - endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); - mockAgentService = startContract.agentService!; - - registerEndpointRoutes(routerMock, { - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }); - }); - - afterEach(() => endpointAppContextService.stop()); - - it('test find the latest of all endpoints', async () => { - const mockRequest = httpServerMock.createKibanaRequest({}); - const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); - [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => - path.startsWith(`${HOST_METADATA_LIST_ROUTE}`) - )!; - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ - authRequired: true, - tags: ['access:securitySolution'], - }); - expect(mockResponse.ok).toBeCalled(); - const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; - expect(endpointResultList.hosts.length).toEqual(1); - expect(endpointResultList.total).toEqual(1); - expect(endpointResultList.request_page_index).toEqual(0); - expect(endpointResultList.request_page_size).toEqual(10); - expect(endpointResultList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_1 - ); - }); - - it('should return a single endpoint with status healthy', async () => { - const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - const mockRequest = httpServerMock.createKibanaRequest({ - params: { id: response.hits.hits[0]._id }, - }); - - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online'); - mockAgentService.getAgent = jest.fn().mockReturnValue(({ - active: true, - } as unknown) as Agent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); - - [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => - path.startsWith(`${HOST_METADATA_LIST_ROUTE}`) - )!; - - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ - authRequired: true, - tags: ['access:securitySolution'], - }); - expect(mockResponse.ok).toBeCalled(); - const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; - expect(result).toHaveProperty('metadata.Endpoint'); - expect(result.host_status).toEqual(HostStatus.HEALTHY); - expect(result.query_strategy_version).toEqual(MetadataQueryStrategyVersions.VERSION_1); - }); - }); - describe('with new transform package', () => { beforeEach(() => { endpointAppContextService = new EndpointAppContextService(); @@ -254,9 +161,6 @@ describe('test endpoint route', () => { expect(endpointResultList.total).toEqual(1); expect(endpointResultList.request_page_index).toEqual(0); expect(endpointResultList.request_page_size).toEqual(10); - expect(endpointResultList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_2 - ); }); it('test find the latest of all endpoints with paging properties', async () => { @@ -311,9 +215,6 @@ describe('test endpoint route', () => { expect(endpointResultList.total).toEqual(1); expect(endpointResultList.request_page_index).toEqual(10); expect(endpointResultList.request_page_size).toEqual(10); - expect(endpointResultList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_2 - ); }); it('test find the latest of all endpoints with paging and filters properties', async () => { @@ -405,9 +306,6 @@ describe('test endpoint route', () => { expect(endpointResultList.total).toEqual(1); expect(endpointResultList.request_page_index).toEqual(10); expect(endpointResultList.request_page_size).toEqual(10); - expect(endpointResultList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_2 - ); }); describe('Endpoint Details route', () => { @@ -475,7 +373,6 @@ describe('test endpoint route', () => { const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; expect(result).toHaveProperty('metadata.Endpoint'); expect(result.host_status).toEqual(HostStatus.HEALTHY); - expect(result.query_strategy_version).toEqual(MetadataQueryStrategyVersions.VERSION_2); }); it('should return a single endpoint with status unhealthy when AgentService throw 404', async () => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts deleted file mode 100644 index 29b2c231cc4a57..00000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts +++ /dev/null @@ -1,456 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - KibanaResponseFactory, - RequestHandler, - RouteConfig, - SavedObjectsClientContract, - SavedObjectsErrorHelpers, -} from '../../../../../../../src/core/server'; -import { - ClusterClientMock, - ScopedClusterClientMock, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../src/core/server/elasticsearch/client/mocks'; -import { - elasticsearchServiceMock, - httpServerMock, - httpServiceMock, - loggingSystemMock, - savedObjectsClientMock, -} from '../../../../../../../src/core/server/mocks'; -import { - HostInfo, - HostResultList, - HostStatus, - MetadataQueryStrategyVersions, -} from '../../../../common/endpoint/types'; -import { registerEndpointRoutes, METADATA_REQUEST_V1_ROUTE } from './index'; -import { - createMockEndpointAppContextServiceStartContract, - createMockPackageService, - createRouteHandlerContext, -} from '../../mocks'; -import { - EndpointAppContextService, - EndpointAppContextServiceStartContract, -} from '../../endpoint_app_context_services'; -import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; -import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; -import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; -import { Agent } from '../../../../../fleet/common/types/models'; -import { createV1SearchResponse } from './support/test_support'; -import { PackageService } from '../../../../../fleet/server/services'; -import type { SecuritySolutionPluginRouter } from '../../../types'; -import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; - -describe('test endpoint route v1', () => { - let routerMock: jest.Mocked; - let mockResponse: jest.Mocked; - let mockClusterClient: ClusterClientMock; - let mockScopedClient: ScopedClusterClientMock; - let mockSavedObjectClient: jest.Mocked; - let mockPackageService: jest.Mocked; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let routeHandler: RequestHandler; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let routeConfig: RouteConfig; - // tests assume that fleet is enabled, and thus agentService is available - let mockAgentService: Required< - ReturnType - >['agentService']; - let endpointAppContextService: EndpointAppContextService; - let startContract: EndpointAppContextServiceStartContract; - const noUnenrolledAgent = { - agents: [], - total: 0, - page: 1, - perPage: 1, - }; - - beforeEach(() => { - mockClusterClient = elasticsearchServiceMock.createClusterClient(); - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); - mockSavedObjectClient = savedObjectsClientMock.create(); - mockClusterClient.asScoped.mockReturnValue(mockScopedClient); - routerMock = httpServiceMock.createRouter(); - mockResponse = httpServerMock.createResponseFactory(); - endpointAppContextService = new EndpointAppContextService(); - mockPackageService = createMockPackageService(); - mockPackageService.getInstallation.mockReturnValue(Promise.resolve(undefined)); - startContract = createMockEndpointAppContextServiceStartContract(); - endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); - mockAgentService = startContract.agentService!; - - (startContract.packagePolicyService as jest.Mocked).list.mockImplementation( - () => { - return Promise.resolve({ - items: [], - total: 0, - page: 1, - perPage: 1000, - }); - } - ); - - registerEndpointRoutes(routerMock, { - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }); - }); - - afterEach(() => endpointAppContextService.stop()); - - it('test find the latest of all endpoints', async () => { - const mockRequest = httpServerMock.createKibanaRequest({}); - const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); - [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => - path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) - )!; - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] }); - expect(mockResponse.ok).toBeCalled(); - const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; - expect(endpointResultList.hosts.length).toEqual(1); - expect(endpointResultList.total).toEqual(1); - expect(endpointResultList.request_page_index).toEqual(0); - expect(endpointResultList.request_page_size).toEqual(10); - expect(endpointResultList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_1 - ); - }); - - it('test find the latest of all endpoints with paging properties', async () => { - const mockRequest = httpServerMock.createKibanaRequest({ - body: { - paging_properties: [ - { - page_size: 10, - }, - { - page_index: 1, - }, - ], - }, - }); - - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ - body: createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()), - }) - ); - [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => - path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) - )!; - - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect( - (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool - .must_not - ).toContainEqual({ - terms: { - 'elastic.agent.id': [ - '00000000-0000-0000-0000-000000000000', - '11111111-1111-1111-1111-111111111111', - ], - }, - }); - expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] }); - expect(mockResponse.ok).toBeCalled(); - const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; - expect(endpointResultList.hosts.length).toEqual(1); - expect(endpointResultList.total).toEqual(1); - expect(endpointResultList.request_page_index).toEqual(10); - expect(endpointResultList.request_page_size).toEqual(10); - expect(endpointResultList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_1 - ); - }); - - it('test find the latest of all endpoints with paging and filters properties', async () => { - const mockRequest = httpServerMock.createKibanaRequest({ - body: { - paging_properties: [ - { - page_size: 10, - }, - { - page_index: 1, - }, - ], - - filters: { kql: 'not host.ip:10.140.73.246' }, - }, - }); - - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ - body: createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()), - }) - ); - [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => - path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) - )!; - - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toBeCalled(); - // needs to have the KQL filter passed through - expect( - (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must - ).toContainEqual({ - bool: { - must_not: { - bool: { - should: [ - { - match: { - 'host.ip': '10.140.73.246', - }, - }, - ], - minimum_should_match: 1, - }, - }, - }, - }); - // and unenrolled should be filtered out. - expect( - (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must - ).toContainEqual({ - bool: { - must_not: [ - { - terms: { - 'elastic.agent.id': [ - '00000000-0000-0000-0000-000000000000', - '11111111-1111-1111-1111-111111111111', - ], - }, - }, - { - terms: { - // we actually don't care about HostDetails in v1 queries, but - // harder to set up the expectation to ignore its inclusion succinctly - 'HostDetails.elastic.agent.id': [ - '00000000-0000-0000-0000-000000000000', - '11111111-1111-1111-1111-111111111111', - ], - }, - }, - ], - }, - }); - expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] }); - expect(mockResponse.ok).toBeCalled(); - const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; - expect(endpointResultList.hosts.length).toEqual(1); - expect(endpointResultList.total).toEqual(1); - expect(endpointResultList.request_page_index).toEqual(10); - expect(endpointResultList.request_page_size).toEqual(10); - expect(endpointResultList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_1 - ); - }); - - describe('Endpoint Details route', () => { - it('should return 404 on no results', async () => { - const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } }); - - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: createV1SearchResponse() }) - ); - - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.getAgent = jest.fn().mockReturnValue(({ - active: true, - } as unknown) as Agent); - - [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => - path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) - )!; - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ - authRequired: true, - tags: ['access:securitySolution'], - }); - expect(mockResponse.notFound).toBeCalled(); - const message = mockResponse.notFound.mock.calls[0][0]?.body; - expect(message).toEqual('Endpoint Not Found'); - }); - - it('should return a single endpoint with status healthy', async () => { - const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - const mockRequest = httpServerMock.createKibanaRequest({ - params: { id: response.hits.hits[0]._id }, - }); - - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online'); - mockAgentService.getAgent = jest.fn().mockReturnValue(({ - active: true, - } as unknown) as Agent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); - - [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => - path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) - )!; - - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ - authRequired: true, - tags: ['access:securitySolution'], - }); - expect(mockResponse.ok).toBeCalled(); - const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; - expect(result).toHaveProperty('metadata.Endpoint'); - expect(result.host_status).toEqual(HostStatus.HEALTHY); - }); - - it('should return a single endpoint with status unhealthy when AgentService throw 404', async () => { - const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - - const mockRequest = httpServerMock.createKibanaRequest({ - params: { id: response.hits.hits[0]._id }, - }); - - mockAgentService.getAgentStatusById = jest.fn().mockImplementation(() => { - SavedObjectsErrorHelpers.createGenericNotFoundError(); - }); - - mockAgentService.getAgent = jest.fn().mockImplementation(() => { - SavedObjectsErrorHelpers.createGenericNotFoundError(); - }); - - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); - - [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => - path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) - )!; - - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ - authRequired: true, - tags: ['access:securitySolution'], - }); - expect(mockResponse.ok).toBeCalled(); - const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; - expect(result.host_status).toEqual(HostStatus.UNHEALTHY); - }); - - it('should return a single endpoint with status unhealthy when status is not offline, online or enrolling', async () => { - const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - - const mockRequest = httpServerMock.createKibanaRequest({ - params: { id: response.hits.hits[0]._id }, - }); - - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('warning'); - mockAgentService.getAgent = jest.fn().mockReturnValue(({ - active: true, - } as unknown) as Agent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); - - [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => - path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) - )!; - - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ - authRequired: true, - tags: ['access:securitySolution'], - }); - expect(mockResponse.ok).toBeCalled(); - const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; - expect(result.host_status).toEqual(HostStatus.UNHEALTHY); - }); - - it('should throw error when endpoint agent is not active', async () => { - const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - - const mockRequest = httpServerMock.createKibanaRequest({ - params: { id: response.hits.hits[0]._id }, - }); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); - mockAgentService.getAgent = jest.fn().mockReturnValue(({ - active: false, - } as unknown) as Agent); - - [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => - path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) - )!; - - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(mockResponse.customError).toBeCalled(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts index e790c1de1a5b8e..87de5a540ea991 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts @@ -11,38 +11,29 @@ import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { metadataCurrentIndexPattern } from '../../../../common/endpoint/constants'; import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; -import { metadataQueryStrategyV2 } from './support/query_strategies'; import { get } from 'lodash'; describe('query builder', () => { describe('MetadataListESQuery', () => { it('queries the correct index', async () => { const mockRequest = httpServerMock.createKibanaRequest({ body: {} }); - const query = await kibanaRequestToMetadataListESQuery( - mockRequest, - { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }, - metadataQueryStrategyV2() - ); + const query = await kibanaRequestToMetadataListESQuery(mockRequest, { + logFactory: loggingSystemMock.create(), + service: new EndpointAppContextService(), + config: () => Promise.resolve(createMockConfig()), + experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), + }); expect(query.index).toEqual(metadataCurrentIndexPattern); }); it('sorts using *event.created', async () => { const mockRequest = httpServerMock.createKibanaRequest({ body: {} }); - const query = await kibanaRequestToMetadataListESQuery( - mockRequest, - { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }, - metadataQueryStrategyV2() - ); + const query = await kibanaRequestToMetadataListESQuery(mockRequest, { + logFactory: loggingSystemMock.create(), + service: new EndpointAppContextService(), + config: () => Promise.resolve(createMockConfig()), + experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), + }); expect(query.body.sort).toContainEqual({ 'event.created': { order: 'desc', @@ -61,16 +52,12 @@ describe('query builder', () => { const mockRequest = httpServerMock.createKibanaRequest({ body: {}, }); - const query = await kibanaRequestToMetadataListESQuery( - mockRequest, - { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }, - metadataQueryStrategyV2() - ); + const query = await kibanaRequestToMetadataListESQuery(mockRequest, { + logFactory: loggingSystemMock.create(), + service: new EndpointAppContextService(), + config: () => Promise.resolve(createMockConfig()), + experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), + }); expect(query.body.query).toHaveProperty('match_all'); }); @@ -87,7 +74,6 @@ describe('query builder', () => { config: () => Promise.resolve(createMockConfig()), experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), }, - metadataQueryStrategyV2(), { unenrolledAgentIds: [unenrolledElasticAgentId], } @@ -111,16 +97,12 @@ describe('query builder', () => { filters: { kql: 'not host.ip:10.140.73.246' }, }, }); - const query = await kibanaRequestToMetadataListESQuery( - mockRequest, - { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }, - metadataQueryStrategyV2() - ); + const query = await kibanaRequestToMetadataListESQuery(mockRequest, { + logFactory: loggingSystemMock.create(), + service: new EndpointAppContextService(), + config: () => Promise.resolve(createMockConfig()), + experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), + }); expect(query.body.query.bool.must).toContainEqual({ bool: { @@ -160,7 +142,6 @@ describe('query builder', () => { createMockConfig().enableExperimental ), }, - metadataQueryStrategyV2(), { unenrolledAgentIds: [unenrolledElasticAgentId], } @@ -197,13 +178,13 @@ describe('query builder', () => { describe('MetadataGetQuery', () => { it('searches the correct index', () => { - const query = getESQueryHostMetadataByID('nonsense-id', metadataQueryStrategyV2()); + const query = getESQueryHostMetadataByID('nonsense-id'); expect(query.index).toEqual(metadataCurrentIndexPattern); }); it('searches for the correct ID', () => { const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; - const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV2()); + const query = getESQueryHostMetadataByID(mockID); expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({ term: { 'agent.id': mockID }, @@ -212,7 +193,7 @@ describe('query builder', () => { it('supports HostDetails in schema for backwards compat', () => { const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; - const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV2()); + const query = getESQueryHostMetadataByID(mockID); expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({ term: { 'HostDetails.agent.id': mockID }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index f0950e5fb79ba7..99ec1d10227472 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -6,9 +6,10 @@ */ import type { estypes } from '@elastic/elasticsearch'; +import { metadataCurrentIndexPattern } from '../../../../common/endpoint/constants'; import { KibanaRequest } from '../../../../../../../src/core/server'; import { esKuery } from '../../../../../../../src/plugins/data/server'; -import { EndpointAppContext, MetadataQueryStrategy } from '../../types'; +import { EndpointAppContext } from '../../types'; export interface QueryBuilderOptions { unenrolledAgentIds?: string[]; @@ -39,7 +40,6 @@ export async function kibanaRequestToMetadataListESQuery( // eslint-disable-next-line @typescript-eslint/no-explicit-any request: KibanaRequest, endpointAppContext: EndpointAppContext, - metadataQueryStrategy: MetadataQueryStrategy, queryBuilderOptions?: QueryBuilderOptions // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise> { @@ -49,16 +49,15 @@ export async function kibanaRequestToMetadataListESQuery( body: { query: buildQueryBody( request, - metadataQueryStrategy, queryBuilderOptions?.unenrolledAgentIds!, queryBuilderOptions?.statusAgentIDs! ), - ...metadataQueryStrategy.extraBodyProperties, + track_total_hits: true, sort: MetadataSortMethod, }, from: pagingProperties.pageIndex * pagingProperties.pageSize, size: pagingProperties.pageSize, - index: metadataQueryStrategy.index, + index: metadataCurrentIndexPattern, }; } @@ -86,7 +85,6 @@ async function getPagingProperties( function buildQueryBody( // eslint-disable-next-line @typescript-eslint/no-explicit-any request: KibanaRequest, - metadataQueryStrategy: MetadataQueryStrategy, unerolledAgentIds: string[] | undefined, statusAgentIDs: string[] | undefined // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -144,10 +142,7 @@ function buildQueryBody( }; } -export function getESQueryHostMetadataByID( - agentID: string, - metadataQueryStrategy: MetadataQueryStrategy -): estypes.SearchRequest { +export function getESQueryHostMetadataByID(agentID: string): estypes.SearchRequest { return { body: { query: { @@ -167,14 +162,11 @@ export function getESQueryHostMetadataByID( sort: MetadataSortMethod, size: 1, }, - index: metadataQueryStrategy.index, + index: metadataCurrentIndexPattern, }; } -export function getESQueryHostMetadataByIDs( - agentIDs: string[], - metadataQueryStrategy: MetadataQueryStrategy -) { +export function getESQueryHostMetadataByIDs(agentIDs: string[]) { return { body: { query: { @@ -193,6 +185,6 @@ export function getESQueryHostMetadataByIDs( }, sort: MetadataSortMethod, }, - index: metadataQueryStrategy.index, + index: metadataCurrentIndexPattern, }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts deleted file mode 100644 index c18c585cd3d34b..00000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { httpServerMock, loggingSystemMock } from '../../../../../../../src/core/server/mocks'; -import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders'; -import { EndpointAppContextService } from '../../endpoint_app_context_services'; -import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; -import { metadataIndexPattern } from '../../../../common/endpoint/constants'; -import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; -import { metadataQueryStrategyV1 } from './support/query_strategies'; -import { get } from 'lodash'; - -describe('query builder v1', () => { - describe('MetadataListESQuery', () => { - it('test default query params for all endpoints metadata when no params or body is provided', async () => { - const mockRequest = httpServerMock.createKibanaRequest({ - body: {}, - }); - const query = await kibanaRequestToMetadataListESQuery( - mockRequest, - { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }, - metadataQueryStrategyV1() - ); - - expect(query.body.query).toHaveProperty('match_all'); // no filtering - expect(query.body.collapse).toEqual({ - field: 'agent.id', - inner_hits: { - name: 'most_recent', - size: 1, - sort: [{ 'event.created': 'desc' }], - }, - }); - expect(query.body.aggs).toEqual({ - total: { - cardinality: { - field: 'agent.id', - }, - }, - }); - expect(query.index).toEqual(metadataIndexPattern); - }); - - it( - 'test default query params for all endpoints metadata when no params or body is provided ' + - 'with unenrolled host ids excluded', - async () => { - const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672'; - const mockRequest = httpServerMock.createKibanaRequest({ - body: {}, - }); - const query = await kibanaRequestToMetadataListESQuery( - mockRequest, - { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue( - createMockConfig().enableExperimental - ), - }, - metadataQueryStrategyV1(), - { - unenrolledAgentIds: [unenrolledElasticAgentId], - } - ); - expect(Object.keys(query.body.query.bool)).toEqual(['must_not']); // only filtering out unenrolled - expect(query.body.query.bool.must_not).toContainEqual({ - terms: { 'elastic.agent.id': [unenrolledElasticAgentId] }, - }); - } - ); - }); - - describe('test query builder with kql filter', () => { - it('test default query params for all endpoints metadata when body filter is provided', async () => { - const mockRequest = httpServerMock.createKibanaRequest({ - body: { - filters: { kql: 'not host.ip:10.140.73.246' }, - }, - }); - const query = await kibanaRequestToMetadataListESQuery( - mockRequest, - { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }, - metadataQueryStrategyV1() - ); - expect(query.body.query.bool.must).toHaveLength(1); // should not be any other filtering happening - expect(query.body.query.bool.must).toContainEqual({ - bool: { - must_not: { - bool: { - should: [ - { - match: { - 'host.ip': '10.140.73.246', - }, - }, - ], - minimum_should_match: 1, - }, - }, - }, - }); - }); - - it( - 'test default query params for all endpoints endpoint metadata excluding unerolled endpoint ' + - 'and when body filter is provided', - async () => { - const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672'; - const mockRequest = httpServerMock.createKibanaRequest({ - body: { - filters: { kql: 'not host.ip:10.140.73.246' }, - }, - }); - const query = await kibanaRequestToMetadataListESQuery( - mockRequest, - { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue( - createMockConfig().enableExperimental - ), - }, - metadataQueryStrategyV1(), - { - unenrolledAgentIds: [unenrolledElasticAgentId], - } - ); - - expect(query.body.query.bool.must.length).toBeGreaterThan(1); - // unenrollment filter should be there - expect(query.body.query.bool.must).toContainEqual({ - bool: { - must_not: [ - { terms: { 'elastic.agent.id': [unenrolledElasticAgentId] } }, - // below is not actually necessary behavior for v1, but hard to structure the test to ignore it - { terms: { 'HostDetails.elastic.agent.id': [unenrolledElasticAgentId] } }, - ], - }, - }); - // and KQL should also be there - expect(query.body.query.bool.must).toContainEqual({ - bool: { - must_not: { - bool: { - should: [ - { - match: { - 'host.ip': '10.140.73.246', - }, - }, - ], - minimum_should_match: 1, - }, - }, - }, - }); - } - ); - }); - - describe('MetadataGetQuery', () => { - it('searches for the correct ID', () => { - const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; - const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV1()); - - expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({ - term: { 'agent.id': mockID }, - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts index 506c02fc2f1ec1..2d7bff4a53f3f0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts @@ -6,102 +6,39 @@ */ import { SearchResponse } from '@elastic/elasticsearch/api/types'; -import { - metadataCurrentIndexPattern, - metadataIndexPattern, -} from '../../../../../common/endpoint/constants'; -import { HostMetadata, MetadataQueryStrategyVersions } from '../../../../../common/endpoint/types'; -import { HostListQueryResult, HostQueryResult, MetadataQueryStrategy } from '../../../types'; +import { HostMetadata } from '../../../../../common/endpoint/types'; +import { HostListQueryResult, HostQueryResult } from '../../../types'; -export function metadataQueryStrategyV1(): MetadataQueryStrategy { - return { - index: metadataIndexPattern, - extraBodyProperties: { - collapse: { - field: 'agent.id', - inner_hits: { - name: 'most_recent', - size: 1, - sort: [{ 'event.created': 'desc' }], - }, - }, - aggs: { - total: { - cardinality: { - field: 'agent.id', - }, - }, - }, - }, - queryResponseToHostListResult: ( - searchResponse: SearchResponse - ): HostListQueryResult => { - const response = searchResponse as SearchResponse; - return { - resultLength: - ((response?.aggregations?.total as unknown) as { value?: number; relation: string }) - ?.value || 0, - resultList: response.hits.hits - .map((hit) => hit.inner_hits?.most_recent.hits.hits) - .flatMap((data) => data) - .map((entry) => (entry?._source ?? {}) as HostMetadata), - queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_1, - }; - }, - queryResponseToHostResult: (searchResponse: SearchResponse): HostQueryResult => { - const response = searchResponse as SearchResponse; - return { - resultLength: response.hits.hits.length, - result: response.hits.hits.length > 0 ? response.hits.hits[0]._source : undefined, - queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_1, - }; - }, - }; +// remove the top-level 'HostDetails' property if found, from previous schemas +function stripHostDetails(host: HostMetadata | { HostDetails: HostMetadata }): HostMetadata { + return 'HostDetails' in host ? host.HostDetails : host; } -export function metadataQueryStrategyV2(): MetadataQueryStrategy { +export const queryResponseToHostResult = ( + searchResponse: SearchResponse +): HostQueryResult => { + const response = searchResponse as SearchResponse; return { - index: metadataCurrentIndexPattern, - extraBodyProperties: { - track_total_hits: true, - }, - queryResponseToHostListResult: ( - searchResponse: SearchResponse - ): HostListQueryResult => { - const response = searchResponse as SearchResponse< - HostMetadata | { HostDetails: HostMetadata } - >; - const list = - response.hits.hits.length > 0 - ? response.hits.hits.map((entry) => stripHostDetails(entry?._source as HostMetadata)) - : []; - - return { - resultLength: - ((response.hits?.total as unknown) as { value: number; relation: string }).value || 0, - resultList: list, - queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_2, - }; - }, - queryResponseToHostResult: ( - searchResponse: SearchResponse - ): HostQueryResult => { - const response = searchResponse as SearchResponse< - HostMetadata | { HostDetails: HostMetadata } - >; - return { - resultLength: response.hits.hits.length, - result: - response.hits.hits.length > 0 - ? stripHostDetails(response.hits.hits[0]._source as HostMetadata) - : undefined, - queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_2, - }; - }, + resultLength: response.hits.hits.length, + result: + response.hits.hits.length > 0 + ? stripHostDetails(response.hits.hits[0]._source as HostMetadata) + : undefined, }; -} +}; -// remove the top-level 'HostDetails' property if found, from previous schemas -function stripHostDetails(host: HostMetadata | { HostDetails: HostMetadata }): HostMetadata { - return 'HostDetails' in host ? host.HostDetails : host; -} +export const queryResponseToHostListResult = ( + searchResponse: SearchResponse +): HostListQueryResult => { + const response = searchResponse as SearchResponse; + const list = + response.hits.hits.length > 0 + ? response.hits.hits.map((entry) => stripHostDetails(entry?._source as HostMetadata)) + : []; + + return { + resultLength: + ((response.hits?.total as unknown) as { value: number; relation: string }).value || 0, + resultList: list, + }; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/test_support.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/test_support.ts index bc23c253c43478..a0530590f5f9fe 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/test_support.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/test_support.ts @@ -8,62 +8,6 @@ import { SearchResponse } from 'elasticsearch'; import { HostMetadata } from '../../../../../common/endpoint/types'; -export function createV1SearchResponse(hostMetadata?: HostMetadata): SearchResponse { - return ({ - took: 15, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 5, - relation: 'eq', - }, - max_score: null, - hits: hostMetadata - ? [ - { - _index: 'metrics-endpoint.metadata-default', - _id: '8FhM0HEBYyRTvb6lOQnw', - _score: null, - _source: hostMetadata, - sort: [1588337587997], - inner_hits: { - most_recent: { - hits: { - total: { - value: 2, - relation: 'eq', - }, - max_score: null, - hits: [ - { - _index: 'metrics-endpoint.metadata-default', - _id: 'W6Vo1G8BYQH1gtPUgYkC', - _score: null, - _source: hostMetadata, - sort: [1579816615336], - }, - ], - }, - }, - }, - }, - ] - : [], - }, - aggregations: { - total: { - value: 1, - }, - }, - } as unknown) as SearchResponse; -} - export function createV2SearchResponse(hostMetadata?: HostMetadata): SearchResponse { return ({ took: 15, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata.ts index 0ca1983aa68d51..1a5515d8122f18 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata.ts @@ -10,20 +10,15 @@ import { SearchResponse } from 'elasticsearch'; import { HostMetadata } from '../../../common/endpoint/types'; import { SecuritySolutionRequestHandlerContext } from '../../types'; import { getESQueryHostMetadataByIDs } from '../routes/metadata/query_builders'; -import { EndpointAppContext } from '../types'; +import { queryResponseToHostListResult } from '../routes/metadata/support/query_strategies'; export async function getMetadataForEndpoints( endpointIDs: string[], - requestHandlerContext: SecuritySolutionRequestHandlerContext, - endpointAppContext: EndpointAppContext + requestHandlerContext: SecuritySolutionRequestHandlerContext ): Promise { - const queryStrategy = await endpointAppContext.service - ?.getMetadataService() - ?.queryStrategy(requestHandlerContext.core.savedObjects.client); - - const query = getESQueryHostMetadataByIDs(endpointIDs, queryStrategy!); + const query = getESQueryHostMetadataByIDs(endpointIDs); const esClient = requestHandlerContext.core.elasticsearch.client.asCurrentUser; const { body } = await esClient.search(query as SearchRequest); - const hosts = queryStrategy!.queryResponseToHostListResult(body as SearchResponse); + const hosts = queryResponseToHostListResult(body as SearchResponse); return hosts.resultList; } diff --git a/x-pack/plugins/security_solution/server/endpoint/types.ts b/x-pack/plugins/security_solution/server/endpoint/types.ts index 6076aa9af635bf..bc52b759b9f0ab 100644 --- a/x-pack/plugins/security_solution/server/endpoint/types.ts +++ b/x-pack/plugins/security_solution/server/endpoint/types.ts @@ -7,11 +7,9 @@ import { LoggerFactory } from 'kibana/server'; -import { SearchResponse } from '@elastic/elasticsearch/api/types'; -import { JsonObject } from '@kbn/common-utils'; import { ConfigType } from '../config'; import { EndpointAppContextService } from './endpoint_app_context_services'; -import { HostMetadata, MetadataQueryStrategyVersions } from '../../common/endpoint/types'; +import { HostMetadata } from '../../common/endpoint/types'; import { ExperimentalFeatures } from '../../common/experimental_features'; /** @@ -31,20 +29,9 @@ export interface EndpointAppContext { export interface HostListQueryResult { resultLength: number; resultList: HostMetadata[]; - queryStrategyVersion: MetadataQueryStrategyVersions; } export interface HostQueryResult { resultLength: number; result: HostMetadata | undefined; - queryStrategyVersion: MetadataQueryStrategyVersions; -} - -export interface MetadataQueryStrategy { - index: string; - extraBodyProperties?: JsonObject; - queryResponseToHostListResult: ( - searchResponse: SearchResponse - ) => HostListQueryResult; - queryResponseToHostResult: (searchResponse: SearchResponse) => HostQueryResult; } diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts index f4d942f733c1db..9b9f49a167397f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts @@ -199,10 +199,10 @@ export const getHostEndpoint = async ( }; const endpointData = id != null && metadataRequestContext.endpointAppContextService.getAgentService() != null - ? await getHostMetaData(metadataRequestContext, id, undefined) + ? await getHostMetaData(metadataRequestContext, id) : null; - const fleetAgentId = endpointData?.metadata.elastic.agent.id; + const fleetAgentId = endpointData?.elastic.agent.id; const [fleetAgentStatus, pendingActions] = !fleetAgentId ? [undefined, {}] : await Promise.all([ @@ -214,13 +214,13 @@ export const getHostEndpoint = async ( }), ]); - return endpointData != null && endpointData.metadata + return endpointData != null && endpointData ? { - endpointPolicy: endpointData.metadata.Endpoint.policy.applied.name, - policyStatus: endpointData.metadata.Endpoint.policy.applied.status, - sensorVersion: endpointData.metadata.agent.version, + endpointPolicy: endpointData.Endpoint.policy.applied.name, + policyStatus: endpointData.Endpoint.policy.applied.status, + sensorVersion: endpointData.agent.version, elasticAgentStatus: fleetAgentStatusToEndpointHostStatus(fleetAgentStatus!), - isolation: endpointData.metadata.Endpoint.state?.isolation ?? false, + isolation: endpointData.Endpoint.state?.isolation ?? false, pendingActions, } : null; diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index 1a52bd18f80af4..e1763b6ad44042 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -29,7 +29,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider }); loadTestFile(require.resolve('./resolver/index')); loadTestFile(require.resolve('./metadata')); - loadTestFile(require.resolve('./metadata_v1')); loadTestFile(require.resolve('./policy')); loadTestFile(require.resolve('./package')); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index b5d98c115d1940..1f57cd1b6db347 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -12,7 +12,6 @@ import { deleteAllDocsFromMetadataIndex, deleteMetadataStream, } from './data_stream_helper'; -import { MetadataQueryStrategyVersions } from '../../../plugins/security_solution/common/endpoint/types'; import { HOST_METADATA_LIST_ROUTE } from '../../../plugins/security_solution/common/endpoint/constants'; /** @@ -88,7 +87,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(1); expect(body.request_page_index).to.eql(1); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2); }); /* test that when paging properties produces no result, the total should reflect the actual number of metadata @@ -113,7 +111,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.hosts.length).to.eql(0); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(30); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2); }); it('metadata api should return 400 when pagingProperties is below boundaries.', async () => { @@ -148,7 +145,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.hosts.length).to.eql(2); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2); }); it('metadata api should return page based on filters and paging passed.', async () => { @@ -186,7 +182,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.hosts.length).to.eql(2); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2); }); it('metadata api should return page based on host.os.Ext.variant filter.', async () => { @@ -208,7 +203,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.hosts.length).to.eql(2); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2); }); it('metadata api should return the latest event for all the events for an endpoint', async () => { @@ -231,7 +225,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2); }); it('metadata api should return the latest event for all the events where policy status is not success', async () => { @@ -275,7 +268,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2); }); it('metadata api should return all hosts when filter is empty string', async () => { @@ -292,7 +284,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.hosts.length).to.eql(numberOfHostsInFixture); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2); }); }); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata_v1.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata_v1.ts deleted file mode 100644 index d8cf1a11fac0a5..00000000000000 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata_v1.ts +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; -import { deleteMetadataStream } from './data_stream_helper'; -import { METADATA_REQUEST_V1_ROUTE } from '../../../plugins/security_solution/server/endpoint/routes/metadata'; -import { MetadataQueryStrategyVersions } from '../../../plugins/security_solution/common/endpoint/types'; - -/** - * The number of host documents in the es archive. - */ -const numberOfHostsInFixture = 3; - -export default function ({ getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const supertest = getService('supertest'); - describe('test metadata api v1', () => { - describe(`POST ${METADATA_REQUEST_V1_ROUTE} when index is empty`, () => { - it('metadata api should return empty result when index is empty', async () => { - // the endpoint uses data streams and es archiver does not support deleting them at the moment so we need - // to do it manually - await deleteMetadataStream(getService); - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send() - .expect(200); - expect(body.total).to.eql(0); - expect(body.hosts.length).to.eql(0); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - }); - - describe(`POST ${METADATA_REQUEST_V1_ROUTE} when index is not empty`, () => { - before( - async () => - await esArchiver.load( - 'x-pack/test/functional/es_archives/endpoint/metadata/api_feature', - { useCreate: true } - ) - ); - // the endpoint uses data streams and es archiver does not support deleting them at the moment so we need - // to do it manually - after(async () => await deleteMetadataStream(getService)); - it('metadata api should return one entry for each host with default paging', async () => { - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send() - .expect(200); - expect(body.total).to.eql(numberOfHostsInFixture); - expect(body.hosts.length).to.eql(numberOfHostsInFixture); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - - it('metadata api should return page based on paging properties passed.', async () => { - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - paging_properties: [ - { - page_size: 1, - }, - { - page_index: 1, - }, - ], - }) - .expect(200); - expect(body.total).to.eql(numberOfHostsInFixture); - expect(body.hosts.length).to.eql(1); - expect(body.request_page_size).to.eql(1); - expect(body.request_page_index).to.eql(1); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - - /* test that when paging properties produces no result, the total should reflect the actual number of metadata - in the index. - */ - it('metadata api should return accurate total metadata if page index produces no result', async () => { - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - paging_properties: [ - { - page_size: 10, - }, - { - page_index: 3, - }, - ], - }) - .expect(200); - expect(body.total).to.eql(numberOfHostsInFixture); - expect(body.hosts.length).to.eql(0); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(30); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - - it('metadata api should return 400 when pagingProperties is below boundaries.', async () => { - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - paging_properties: [ - { - page_size: 0, - }, - { - page_index: 1, - }, - ], - }) - .expect(400); - expect(body.message).to.contain('Value must be equal to or greater than [1]'); - }); - - it('metadata api should return page based on filters passed.', async () => { - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - filters: { - kql: 'not host.ip:10.46.229.234', - }, - }) - .expect(200); - expect(body.total).to.eql(2); - expect(body.hosts.length).to.eql(2); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - - it('metadata api should return page based on filters and paging passed.', async () => { - const notIncludedIp = '10.46.229.234'; - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - paging_properties: [ - { - page_size: 10, - }, - { - page_index: 0, - }, - ], - filters: { - kql: `not host.ip:${notIncludedIp}`, - }, - }) - .expect(200); - expect(body.total).to.eql(2); - const resultIps: string[] = [].concat( - ...body.hosts.map((hostInfo: Record) => hostInfo.metadata.host.ip) - ); - expect(resultIps).to.eql([ - '10.192.213.130', - '10.70.28.129', - '10.101.149.26', - '2606:a000:ffc0:39:11ef:37b9:3371:578c', - ]); - expect(resultIps).not.include.eql(notIncludedIp); - expect(body.hosts.length).to.eql(2); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - - it('metadata api should return page based on host.os.Ext.variant filter.', async () => { - const variantValue = 'Windows Pro'; - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - filters: { - kql: `host.os.Ext.variant:${variantValue}`, - }, - }) - .expect(200); - expect(body.total).to.eql(2); - const resultOsVariantValue: Set = new Set( - body.hosts.map((hostInfo: Record) => hostInfo.metadata.host.os.Ext.variant) - ); - expect(Array.from(resultOsVariantValue)).to.eql([variantValue]); - expect(body.hosts.length).to.eql(2); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - - it('metadata api should return the latest event for all the events for an endpoint', async () => { - const targetEndpointIp = '10.46.229.234'; - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - filters: { - kql: `host.ip:${targetEndpointIp}`, - }, - }) - .expect(200); - expect(body.total).to.eql(1); - const resultIp: string = body.hosts[0].metadata.host.ip.filter( - (ip: string) => ip === targetEndpointIp - ); - expect(resultIp).to.eql([targetEndpointIp]); - expect(body.hosts[0].metadata.event.created).to.eql(1618841405309); - expect(body.hosts.length).to.eql(1); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - - it('metadata api should return the latest event for all the events where policy status is not success', async () => { - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - filters: { - kql: `not Endpoint.policy.applied.status:success`, - }, - }) - .expect(200); - const statuses: Set = new Set( - body.hosts.map( - (hostInfo: Record) => hostInfo.metadata.Endpoint.policy.applied.status - ) - ); - expect(statuses.size).to.eql(1); - expect(Array.from(statuses)).to.eql(['failure']); - }); - - it('metadata api should return the endpoint based on the elastic agent id, and status should be unhealthy', async () => { - const targetEndpointId = 'fc0ff548-feba-41b6-8367-65e8790d0eaf'; - const targetElasticAgentId = '023fa40c-411d-4188-a941-4147bfadd095'; - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - filters: { - kql: `elastic.agent.id:${targetElasticAgentId}`, - }, - }) - .expect(200); - expect(body.total).to.eql(1); - const resultHostId: string = body.hosts[0].metadata.host.id; - const resultElasticAgentId: string = body.hosts[0].metadata.elastic.agent.id; - expect(resultHostId).to.eql(targetEndpointId); - expect(resultElasticAgentId).to.eql(targetElasticAgentId); - expect(body.hosts[0].metadata.event.created).to.eql(1618841405309); - expect(body.hosts[0].host_status).to.eql('unhealthy'); - expect(body.hosts.length).to.eql(1); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - - it('metadata api should return all hosts when filter is empty string', async () => { - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - filters: { - kql: '', - }, - }) - .expect(200); - expect(body.total).to.eql(numberOfHostsInFixture); - expect(body.hosts.length).to.eql(numberOfHostsInFixture); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - }); - }); -} From b2b57a240423e9503d1307075d34c73eb8b2a3c9 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 12 Jul 2021 00:02:20 -0700 Subject: [PATCH 73/77] [dev-docs] Add debugging tutorial (#104468) Signed-off-by: Tyler Smalley Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- dev_docs/tutorials/debugging.mdx | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 dev_docs/tutorials/debugging.mdx diff --git a/dev_docs/tutorials/debugging.mdx b/dev_docs/tutorials/debugging.mdx new file mode 100644 index 00000000000000..c0efd249be0668 --- /dev/null +++ b/dev_docs/tutorials/debugging.mdx @@ -0,0 +1,61 @@ +--- +id: kibDevTutorialDebugging +slug: /kibana-dev-docs/tutorial/debugging +title: Debugging in development +summary: Learn how to debug Kibana while running from source +date: 2021-04-26 +tags: ['kibana', 'onboarding', 'dev', 'tutorials', 'debugging'] +--- + +There are multiple ways to go about debugging Kibana when running from source. + +## Debugging using Chrome DevTools + +You will need to run Node using `--inspect` or `--inspect-brk` in order to enable the inspector. Additional information can be found in the [Node.js docs](https://nodejs.org/en/docs/guides/debugging-getting-started/). + +Once Node is running with the inspector enabled, you can open `chrome://inspect` in your Chrome browser. You should see a remote target for the inspector running. Click "inspect". You can now begin using the debugger. + +Next we will go over how to exactly enable the inspector for different aspects of the codebase. + +### Jest Unit Tests + +You will need to run Jest directly from the Node script: + +`node --inspect-brk scripts/jest [TestPathPattern]` + +### Functional Test Runner + +`node --inspect-brk scripts/functional_test_runner` + +### Development Server + +`node --inspect-brk scripts/kibana` + +## Debugging using logging + +When running Kibana, it's sometimes helpful to enable verbose logging. + +`yarn start --verbose` + +Using verbose logging usually results in much more information than you're interested in. The [logging documentation](https://www.elastic.co/guide/en/kibana/current/logging-settings.html) covers ways to change the log level of certain types. + +In the following example of a configuration stored in `config/kibana.dev.yml` we are logging all Elasticsearch queries and any logs created by the Management plugin. + +``` +logging: + appenders: + console: + type: console + layout: + type: pattern + highlight: true + root: + appenders: [default, console] + level: info + + loggers: + - name: plugins.management + level: debug + - name: elasticsearch.query + level: debug +``` \ No newline at end of file From 5256b6d23ef4d05ff3dce1d7d91711c1748c8d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Mon, 12 Jul 2021 11:46:38 +0200 Subject: [PATCH 74/77] Accomodate height of the (#104882) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../sections/agent_policy/edit_package_policy_page/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index b07d76dc6bd8e8..ee529b6865e565 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -430,7 +430,9 @@ export const EditPackagePolicyForm = memo<{ /> )} {configurePackage} - + {/* Extra space to accomodate the EuiBottomBar height */} + + From b8dfcafe38e636ec88b2ca03253cf15cec844d27 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Mon, 12 Jul 2021 11:31:54 +0100 Subject: [PATCH 75/77] [ML] Fixes unnecessary too many buckets warning on anomaly chart embeddable (#105043) * [ML] Fixes unnecessary too many buckets warning on anomaly chart embeddable * [ML] Update jest tests for number of axis ticks. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../explorer_chart_distribution.js | 24 ++++++++++--------- .../explorer_chart_distribution.test.js | 2 +- .../explorer_chart_single_metric.js | 24 ++++++++++--------- .../explorer_chart_single_metric.test.js | 2 +- .../anomaly_explorer_charts_service.ts | 11 +++++++-- 5 files changed, 37 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index 7efd36bbe57c61..27a934fa841fe0 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -270,12 +270,6 @@ export class ExplorerChartDistribution extends React.Component { const tickValuesStart = Math.max(config.selectedEarliest, config.plotEarliest); // +1 ms to account for the ms that was subtracted for query aggregations. const interval = config.selectedLatest - config.selectedEarliest + 1; - const tickValues = getTickValues( - tickValuesStart, - interval, - config.plotEarliest, - config.plotLatest - ); const xAxis = d3.svg .axis() @@ -286,10 +280,18 @@ export class ExplorerChartDistribution extends React.Component { .tickPadding(10) .tickFormat((d) => moment(d).format(xAxisTickFormat)); - // With tooManyBuckets the chart would end up with no x-axis labels - // because the ticks are based on the span of the emphasis section, - // and the highlighted area spans the whole chart. - if (tooManyBuckets === false) { + // With tooManyBuckets, or when the chart is used as an embeddable, + // the chart would end up with no x-axis labels because the ticks are based on the span of the + // emphasis section, and the selected area spans the whole chart. + const useAutoTicks = + tooManyBuckets === true || interval >= config.plotLatest - config.plotEarliest; + if (useAutoTicks === false) { + const tickValues = getTickValues( + tickValuesStart, + interval, + config.plotEarliest, + config.plotLatest + ); xAxis.tickValues(tickValues); } else { xAxis.ticks(numTicksForDateFormat(vizWidth, xAxisTickFormat)); @@ -327,7 +329,7 @@ export class ExplorerChartDistribution extends React.Component { }); } - if (tooManyBuckets === false) { + if (useAutoTicks === false) { removeLabelOverlap(gAxis, tickValuesStart, interval, vizWidth); } } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js index 11a15b192fc520..8d2f66a870c75c 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js @@ -139,7 +139,7 @@ describe('ExplorerChart', () => { expect(+selectedInterval.getAttribute('height')).toBe(166); const xAxisTicks = wrapper.getDOMNode().querySelector('.x').querySelectorAll('.tick'); - expect([...xAxisTicks]).toHaveLength(0); + expect([...xAxisTicks]).toHaveLength(1); const yAxisTicks = wrapper.getDOMNode().querySelector('.y').querySelectorAll('.tick'); expect([...yAxisTicks]).toHaveLength(5); const emphasizedAxisLabel = wrapper diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index dd07a7d6cbdee0..19390017244a8c 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -196,12 +196,6 @@ export class ExplorerChartSingleMetric extends React.Component { const tickValuesStart = Math.max(config.selectedEarliest, config.plotEarliest); // +1 ms to account for the ms that was subtracted for query aggregations. const interval = config.selectedLatest - config.selectedEarliest + 1; - const tickValues = getTickValues( - tickValuesStart, - interval, - config.plotEarliest, - config.plotLatest - ); const xAxis = d3.svg .axis() @@ -212,10 +206,18 @@ export class ExplorerChartSingleMetric extends React.Component { .tickPadding(10) .tickFormat((d) => moment(d).format(xAxisTickFormat)); - // With tooManyBuckets the chart would end up with no x-axis labels - // because the ticks are based on the span of the emphasis section, - // and the highlighted area spans the whole chart. - if (tooManyBuckets === false) { + // With tooManyBuckets, or when the chart is used as an embeddable, + // the chart would end up with no x-axis labels because the ticks are based on the span of the + // emphasis section, and the selected area spans the whole chart. + const useAutoTicks = + tooManyBuckets === true || interval >= config.plotLatest - config.plotEarliest; + if (useAutoTicks === false) { + const tickValues = getTickValues( + tickValuesStart, + interval, + config.plotEarliest, + config.plotLatest + ); xAxis.tickValues(tickValues); } else { xAxis.ticks(numTicksForDateFormat(vizWidth, xAxisTickFormat)); @@ -243,7 +245,7 @@ export class ExplorerChartSingleMetric extends React.Component { axes.append('g').attr('class', 'y axis').call(yAxis); - if (tooManyBuckets === false) { + if (useAutoTicks === false) { removeLabelOverlap(gAxis, tickValuesStart, interval, vizWidth); } } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js index 981f7515d3d706..00172965bc2162 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js @@ -144,7 +144,7 @@ describe('ExplorerChart', () => { expect(+selectedInterval.getAttribute('height')).toBe(166); const xAxisTicks = wrapper.getDOMNode().querySelector('.x').querySelectorAll('.tick'); - expect([...xAxisTicks]).toHaveLength(0); + expect([...xAxisTicks]).toHaveLength(1); const yAxisTicks = wrapper.getDOMNode().querySelector('.y').querySelectorAll('.tick'); expect([...yAxisTicks]).toHaveLength(10); diff --git a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts index afad043fcc4d13..97ddefac860f2d 100644 --- a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts @@ -225,13 +225,20 @@ export class AnomalyExplorerChartsService { chartRange.min = chartRange.min + maxBucketSpanMs; } + // When used as an embeddable, selectedEarliestMs is the start date on the time picker, + // which may be earlier than the time of the first point plotted in the chart (as we plot + // the first full bucket with a start date no earlier than the start). + const selectedEarliestBucketCeil = boundsMin + ? Math.ceil(Math.max(selectedEarliestMs, boundsMin) / maxBucketSpanMs) * maxBucketSpanMs + : Math.ceil(selectedEarliestMs / maxBucketSpanMs) * maxBucketSpanMs; + const selectedLatestBucketStart = boundsMax ? Math.floor(Math.min(selectedLatestMs, boundsMax) / maxBucketSpanMs) * maxBucketSpanMs : Math.floor(selectedLatestMs / maxBucketSpanMs) * maxBucketSpanMs; if ( - (chartRange.min > selectedEarliestMs || chartRange.max < selectedLatestBucketStart) && - chartRange.max - chartRange.min < selectedLatestBucketStart - selectedEarliestMs + (chartRange.min > selectedEarliestBucketCeil || chartRange.max < selectedLatestBucketStart) && + chartRange.max - chartRange.min < selectedLatestBucketStart - selectedEarliestBucketCeil ) { tooManyBuckets = true; } From 216bb5e1b8c8406cd23503619972e5a3ea543790 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Mon, 12 Jul 2021 13:23:08 +0200 Subject: [PATCH 76/77] [load] run puppeteer script before gatling scenarios (#104836) * [load] puppeteer script before load testing * install dependencies after metricbeat configuration Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/scripts/jenkins_build_load_testing.sh | 3 +++ x-pack/test/load/runner.ts | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/test/scripts/jenkins_build_load_testing.sh b/test/scripts/jenkins_build_load_testing.sh index d7c7bda83c9ef2..667540515fc835 100755 --- a/test/scripts/jenkins_build_load_testing.sh +++ b/test/scripts/jenkins_build_load_testing.sh @@ -53,6 +53,9 @@ echo "cloud.auth: ${USER_FROM_VAULT}:${PASS_FROM_VAULT}" >> cfg/metricbeat/metri cp cfg/metricbeat/metricbeat.yml $KIBANA_DIR/metricbeat-install/metricbeat.yml # Disable system monitoring: enabled for now to have more data #mv $KIBANA_DIR/metricbeat-install/modules.d/system.yml $KIBANA_DIR/metricbeat-install/modules.d/system.yml.disabled +echo " -> Building puppeteer project" +cd puppeteer +yarn install && yarn build popd # doesn't persist, also set in kibanaPipeline.groovy diff --git a/x-pack/test/load/runner.ts b/x-pack/test/load/runner.ts index 2d379391b20897..0bea5992f55394 100644 --- a/x-pack/test/load/runner.ts +++ b/x-pack/test/load/runner.ts @@ -18,6 +18,7 @@ const simulationPackage = 'org.kibanaLoadTest.simulation'; const simulationFIleExtension = '.scala'; const gatlingProjectRootPath: string = process.env.GATLING_PROJECT_PATH || resolve(REPO_ROOT, '../kibana-load-testing'); +const puppeteerProjectRootPath: string = resolve(gatlingProjectRootPath, 'puppeteer'); const simulationEntry: string = process.env.GATLING_SIMULATIONS || 'branch.DemoJourney'; if (!Fs.existsSync(gatlingProjectRootPath)) { @@ -52,6 +53,15 @@ export async function GatlingTestRunner({ getService }: FtrProviderContext) { const log = getService('log'); await withProcRunner(log, async (procs) => { + await procs.run('node build/index.js', { + cmd: 'node', + args: ['build/index.js'], + cwd: puppeteerProjectRootPath, + env: { + ...process.env, + }, + wait: true, + }); for (let i = 0; i < simulationClasses.length; i++) { await procs.run('gatling: test', { cmd: 'mvn', From 832d349930be1f8d4a7a47733f413b0ab75303e8 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 12 Jul 2021 14:13:42 +0200 Subject: [PATCH 77/77] [KibanaLegacy] Remove unused stuff and make things async if it is easy (#104638) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove unused stuff and make things async if it is easy * fix problems * load bootstrap in monitoring * load angular bootstrap for saved searches and in unit tests * fix vis_type_table tests * Update x-pack/plugins/monitoring/public/plugin.ts Co-authored-by: Ester Martí Vilaseca * Update x-pack/plugins/monitoring/public/plugin.ts Co-authored-by: Ester Martí Vilaseca Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli Co-authored-by: Ester Martí Vilaseca --- .../doc_table/components/row_headers.test.js | 4 + .../angular/doc_table/doc_table.test.js | 4 + .../application/angular/get_inner_angular.ts | 4 +- .../application/angular/helpers/index.ts | 1 + .../angular/helpers}/promises.d.ts | 0 .../application/angular/helpers}/promises.js | 0 src/plugins/discover/public/plugin.tsx | 2 + .../public/angular/angular_config.tsx | 7 +- .../kibana_legacy/public/angular/index.ts | 2 - src/plugins/kibana_legacy/public/index.ts | 1 - src/plugins/kibana_legacy/public/mocks.ts | 1 + .../kibana_legacy/public/notify/index.ts | 1 - .../notify/toasts/TOAST_NOTIFICATIONS.md | 100 ------------------ .../public/notify/toasts/index.ts | 9 -- .../notify/toasts/toast_notifications.test.ts | 76 ------------- .../notify/toasts/toast_notifications.ts | 37 ------- src/plugins/kibana_legacy/public/plugin.ts | 8 ++ .../kibana_legacy/public/utils/index.ts | 1 - .../kibana_legacy/public/utils/system_api.ts | 40 ------- src/plugins/timelion/public/plugin.ts | 5 +- .../public/legacy/agg_table/agg_table.test.js | 6 +- .../legacy/agg_table/agg_table_group.test.js | 6 +- .../public/legacy/get_inner_angular.ts | 3 - .../paginated_table/paginated_table.test.ts | 5 + .../legacy/table_vis_controller.test.ts | 4 + .../public/legacy/vis_controller.ts | 1 + x-pack/plugins/graph/public/plugin.ts | 7 +- x-pack/plugins/monitoring/public/plugin.ts | 5 +- 28 files changed, 54 insertions(+), 286 deletions(-) rename src/plugins/{kibana_legacy/public/angular => discover/public/application/angular/helpers}/promises.d.ts (100%) rename src/plugins/{kibana_legacy/public/angular => discover/public/application/angular/helpers}/promises.js (100%) delete mode 100644 src/plugins/kibana_legacy/public/notify/toasts/TOAST_NOTIFICATIONS.md delete mode 100644 src/plugins/kibana_legacy/public/notify/toasts/index.ts delete mode 100644 src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.test.ts delete mode 100644 src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.ts delete mode 100644 src/plugins/kibana_legacy/public/utils/system_api.ts diff --git a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js b/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js index a087ac86971838..1a3b34c45d05e2 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js +++ b/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js @@ -19,6 +19,7 @@ import { setScopedHistory, setServices, setDocViewsRegistry } from '../../../../ import { coreMock } from '../../../../../../../core/public/mocks'; import { dataPluginMock } from '../../../../../../data/public/mocks'; import { navigationPluginMock } from '../../../../../../navigation/public/mocks'; +import { initAngularBootstrap } from '../../../../../../kibana_legacy/public/angular_bootstrap'; import { getInnerAngularModule } from '../../get_inner_angular'; import { createBrowserHistory } from 'history'; @@ -41,6 +42,9 @@ describe('Doc Table', () => { // Stub out a minimal mapping of 4 fields let mapping; + beforeAll(async () => { + await initAngularBootstrap(); + }); beforeAll(() => setScopedHistory(createBrowserHistory())); beforeEach(() => { angular.element.prototype.slice = jest.fn(function (index) { diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js b/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js index 1db35ddf18089e..097f32965b1413 100644 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js +++ b/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js @@ -17,6 +17,7 @@ import hits from '../../../__fixtures__/real_hits'; import { coreMock } from '../../../../../../core/public/mocks'; import { dataPluginMock } from '../../../../../data/public/mocks'; import { navigationPluginMock } from '../../../../../navigation/public/mocks'; +import { initAngularBootstrap } from '../../../../../kibana_legacy/public/angular_bootstrap'; import { setScopedHistory, setServices } from '../../../kibana_services'; import { getInnerAngularModule } from '../get_inner_angular'; @@ -54,6 +55,9 @@ describe('docTable', () => { const core = coreMock.createStart(); let $elem; + beforeAll(async () => { + await initAngularBootstrap(); + }); beforeAll(() => setScopedHistory(createBrowserHistory())); beforeEach(() => { angular.element.prototype.slice = jest.fn(() => { diff --git a/src/plugins/discover/public/application/angular/get_inner_angular.ts b/src/plugins/discover/public/application/angular/get_inner_angular.ts index 26d64d5adc8a33..992d82795302b9 100644 --- a/src/plugins/discover/public/application/angular/get_inner_angular.ts +++ b/src/plugins/discover/public/application/angular/get_inner_angular.ts @@ -33,13 +33,12 @@ import { createDocViewerDirective } from './doc_viewer'; import { createDiscoverGridDirective } from './create_discover_grid_directive'; import { createRenderCompleteDirective } from './directives/render_complete'; import { - initAngularBootstrap, configureAppAngularModule, PrivateProvider, - PromiseServiceCreator, registerListenEventListener, watchMultiDecorator, } from '../../../../kibana_legacy/public'; +import { PromiseServiceCreator } from './helpers'; import { DiscoverStartPlugins } from '../../plugin'; import { getScopedHistory } from '../../kibana_services'; import { createDiscoverDirective } from './create_discover_directive'; @@ -54,7 +53,6 @@ export function getInnerAngularModule( deps: DiscoverStartPlugins, context: PluginInitializerContext ) { - initAngularBootstrap(); const module = initializeInnerAngularModule(name, core, deps.navigation, deps.data); configureAppAngularModule(module, { core, env: context.env }, true, getScopedHistory); return module; diff --git a/src/plugins/discover/public/application/angular/helpers/index.ts b/src/plugins/discover/public/application/angular/helpers/index.ts index 3d2c0b1c63b332..6a7f75b7e81a20 100644 --- a/src/plugins/discover/public/application/angular/helpers/index.ts +++ b/src/plugins/discover/public/application/angular/helpers/index.ts @@ -8,3 +8,4 @@ export { formatRow, formatTopLevelObject } from './row_formatter'; export { handleSourceColumnState } from './state_helpers'; +export { PromiseServiceCreator } from './promises'; diff --git a/src/plugins/kibana_legacy/public/angular/promises.d.ts b/src/plugins/discover/public/application/angular/helpers/promises.d.ts similarity index 100% rename from src/plugins/kibana_legacy/public/angular/promises.d.ts rename to src/plugins/discover/public/application/angular/helpers/promises.d.ts diff --git a/src/plugins/kibana_legacy/public/angular/promises.js b/src/plugins/discover/public/application/angular/helpers/promises.js similarity index 100% rename from src/plugins/kibana_legacy/public/angular/promises.js rename to src/plugins/discover/public/application/angular/helpers/promises.js diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 3e31fe1d46d459..1e8a5cdac95efe 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -403,6 +403,7 @@ export class DiscoverPlugin } // this is used by application mount and tests const { getInnerAngularModule } = await import('./application/angular/get_inner_angular'); + await plugins.kibanaLegacy.loadAngularBootstrap(); const module = getInnerAngularModule( innerAngularName, core, @@ -473,6 +474,7 @@ export class DiscoverPlugin throw Error('Discover plugin getEmbeddableInjector: initializeServices is undefined'); } const { core, plugins } = await this.initializeServices(); + await getServices().kibanaLegacy.loadAngularBootstrap(); getServices().kibanaLegacy.loadFontAwesome(); const { getInnerAngularModuleEmbeddable } = await import( './application/angular/get_inner_angular' diff --git a/src/plugins/kibana_legacy/public/angular/angular_config.tsx b/src/plugins/kibana_legacy/public/angular/angular_config.tsx index daecfbc57ea991..48ee6d2db269e6 100644 --- a/src/plugins/kibana_legacy/public/angular/angular_config.tsx +++ b/src/plugins/kibana_legacy/public/angular/angular_config.tsx @@ -13,6 +13,7 @@ import { ILocationProvider, IModule, IRootScopeService, + IRequestConfig, } from 'angular'; import $ from 'jquery'; import { set } from '@elastic/safer-lodash-set'; @@ -22,7 +23,6 @@ import { ChromeBreadcrumb, EnvironmentMode, PackageInfo } from 'kibana/public'; import { History } from 'history'; import { CoreStart } from 'kibana/public'; -import { isSystemApiRequest } from '../utils'; import { formatAngularHttpError, isAngularHttpError } from '../notify/lib'; export interface RouteConfiguration { @@ -38,6 +38,11 @@ export interface RouteConfiguration { requireUICapability?: string; } +function isSystemApiRequest(request: IRequestConfig) { + const { headers } = request; + return headers && !!headers['kbn-system-request']; +} + /** * Detects whether a given angular route is a dummy route that doesn't * require any action. There are two ways this can happen: diff --git a/src/plugins/kibana_legacy/public/angular/index.ts b/src/plugins/kibana_legacy/public/angular/index.ts index d9d8c0c19eb7b1..369495698591d0 100644 --- a/src/plugins/kibana_legacy/public/angular/index.ts +++ b/src/plugins/kibana_legacy/public/angular/index.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -// @ts-ignore -export { PromiseServiceCreator } from './promises'; // @ts-ignore export { watchMultiDecorator } from './watch_multi'; export * from './angular_config'; diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts index 03adb768cde208..ea5172f78a68f3 100644 --- a/src/plugins/kibana_legacy/public/index.ts +++ b/src/plugins/kibana_legacy/public/index.ts @@ -14,7 +14,6 @@ export const plugin = (initializerContext: PluginInitializerContext) => export * from './plugin'; -export { initAngularBootstrap } from './angular_bootstrap'; export { PaginateDirectiveProvider, PaginateControlsDirectiveProvider } from './paginate/paginate'; export * from './angular'; export * from './notify'; diff --git a/src/plugins/kibana_legacy/public/mocks.ts b/src/plugins/kibana_legacy/public/mocks.ts index 40834635cc5704..6116c0682cb3bb 100644 --- a/src/plugins/kibana_legacy/public/mocks.ts +++ b/src/plugins/kibana_legacy/public/mocks.ts @@ -22,6 +22,7 @@ const createStartContract = (): Start => ({ getHideWriteControls: jest.fn(), }, loadFontAwesome: jest.fn(), + loadAngularBootstrap: jest.fn(), }); export const kibanaLegacyPluginMock = { diff --git a/src/plugins/kibana_legacy/public/notify/index.ts b/src/plugins/kibana_legacy/public/notify/index.ts index a243059cb19183..d4dcaa77cc47ad 100644 --- a/src/plugins/kibana_legacy/public/notify/index.ts +++ b/src/plugins/kibana_legacy/public/notify/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -export * from './toasts'; export * from './lib'; diff --git a/src/plugins/kibana_legacy/public/notify/toasts/TOAST_NOTIFICATIONS.md b/src/plugins/kibana_legacy/public/notify/toasts/TOAST_NOTIFICATIONS.md deleted file mode 100644 index de6a51f3927d17..00000000000000 --- a/src/plugins/kibana_legacy/public/notify/toasts/TOAST_NOTIFICATIONS.md +++ /dev/null @@ -1,100 +0,0 @@ -# Toast notifications - -Use this service to surface toasts in the bottom-right corner of the screen. After a brief delay, they'll disappear. They're useful for notifying the user of state changes. See [the EUI docs](https://elastic.github.io/eui/) for more information on toasts and their role within the UI. - -## Importing the module - -```js -import { toastNotifications } from 'ui/notify'; -``` - -## Interface - -### Adding toasts - -For convenience, there are several methods which predefine the appearance of different types of toasts. Use these methods so that the same types of toasts look similar to the user. - -#### Default - -Neutral toast. Tell the user a change in state has occurred, which is not necessarily good or bad. - -```js -toastNotifications.add('Copied to clipboard'); -``` - -#### Success - -Let the user know that an action was successful, such as saving or deleting an object. - -```js -toastNotifications.addSuccess('Your document was saved'); -``` - -#### Warning - -If something OK or good happened, but perhaps wasn't perfect, show a warning toast. - -```js -toastNotifications.addWarning('Your document was saved, but not its edit history'); -``` - -#### Danger - -When the user initiated an action but the action failed, show them a danger toast. - -```js -toastNotifications.addDanger('An error caused your document to be lost'); -``` - -### Removing a toast - -Toasts will automatically be dismissed after a brief delay, but if for some reason you want to dismiss a toast, you can use the returned toast from one of the `add` methods and then pass it to `remove`. - -```js -const toast = toastNotifications.add('Your document was saved'); -toastNotifications.remove(toast); -``` - -### Configuration options - -If you want to configure the toast further you can provide an object instead of a string. The properties of this object correspond to the `propTypes` accepted by the `EuiToast` component. Refer to [the EUI docs](https://elastic.github.io/eui/) for info on these `propTypes`. - -```js -toastNotifications.add({ - title: 'Your document was saved', - text: 'Only you have access to this document', - color: 'success', - iconType: 'check', - 'data-test-subj': 'saveDocumentSuccess', -}); -``` - -Because the underlying components are React, you can use JSX to pass in React elements to the `text` prop. This gives you total flexibility over the content displayed within the toast. - -```js -toastNotifications.add({ - title: 'Your document was saved', - text: ( -
-

- Only you have access to this document. Edit permissions. -

- - -
- ), -}); -``` - -## Use in functional tests - -Functional tests are commonly used to verify that a user action yielded a successful outcome. If you surface a toast to notify the user of this successful outcome, you can place a `data-test-subj` attribute on the toast and use it to check if the toast exists inside of your functional test. This acts as a proxy for verifying the successful outcome. - -```js -toastNotifications.addSuccess({ - title: 'Your document was saved', - 'data-test-subj': 'saveDocumentSuccess', -}); -``` diff --git a/src/plugins/kibana_legacy/public/notify/toasts/index.ts b/src/plugins/kibana_legacy/public/notify/toasts/index.ts deleted file mode 100644 index cdd7df04548fbb..00000000000000 --- a/src/plugins/kibana_legacy/public/notify/toasts/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { ToastNotifications } from './toast_notifications'; diff --git a/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.test.ts b/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.test.ts deleted file mode 100644 index c2c5d9a4fc014a..00000000000000 --- a/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { notificationServiceMock } from '../../../../../core/public/mocks'; -import { ToastNotifications } from './toast_notifications'; -import { Toast } from 'kibana/public'; -import { BehaviorSubject } from 'rxjs'; - -describe('ToastNotifications', () => { - describe('interface', () => { - function setup() { - const toastsMock = notificationServiceMock.createStartContract().toasts; - return { toastNotifications: new ToastNotifications(toastsMock), toastsMock }; - } - - describe('add method', () => { - test('adds a toast', () => { - const { toastNotifications, toastsMock } = setup(); - toastNotifications.add({}); - expect(toastsMock.add).toHaveBeenCalled(); - }); - }); - - describe('remove method', () => { - test('removes a toast', () => { - const { toastNotifications, toastsMock } = setup(); - const fakeToast = {} as Toast; - toastNotifications.remove(fakeToast); - expect(toastsMock.remove).toHaveBeenCalledWith(fakeToast); - }); - }); - - describe('onChange method', () => { - test('callback is called when observable changes', () => { - const toastsMock = notificationServiceMock.createStartContract().toasts; - const toasts$ = new BehaviorSubject([]); - toastsMock.get$.mockReturnValue(toasts$); - const toastNotifications = new ToastNotifications(toastsMock); - const onChangeSpy = jest.fn(); - toastNotifications.onChange(onChangeSpy); - toasts$.next([{ id: 'toast1' }]); - toasts$.next([]); - expect(onChangeSpy).toHaveBeenCalledTimes(2); - }); - }); - - describe('addSuccess method', () => { - test('adds a success toast', () => { - const { toastNotifications, toastsMock } = setup(); - toastNotifications.addSuccess({}); - expect(toastsMock.addSuccess).toHaveBeenCalled(); - }); - }); - - describe('addWarning method', () => { - test('adds a warning toast', () => { - const { toastNotifications, toastsMock } = setup(); - toastNotifications.addWarning({}); - expect(toastsMock.addWarning).toHaveBeenCalled(); - }); - }); - - describe('addDanger method', () => { - test('adds a danger toast', () => { - const { toastNotifications, toastsMock } = setup(); - toastNotifications.addWarning({}); - expect(toastsMock.addWarning).toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.ts b/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.ts deleted file mode 100644 index e7ccbbca07b734..00000000000000 --- a/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { NotificationsSetup, Toast, ToastInput, ErrorToastOptions } from 'kibana/public'; - -export class ToastNotifications { - public list: Toast[] = []; - - private onChangeCallback?: () => void; - - constructor(private readonly toasts: NotificationsSetup['toasts']) { - toasts.get$().subscribe((list) => { - this.list = list; - - if (this.onChangeCallback) { - this.onChangeCallback(); - } - }); - } - - public onChange = (callback: () => void) => { - this.onChangeCallback = callback; - }; - - public add = (toastOrTitle: ToastInput) => this.toasts.add(toastOrTitle); - public remove = (toast: Toast) => this.toasts.remove(toast); - public addSuccess = (toastOrTitle: ToastInput) => this.toasts.addSuccess(toastOrTitle); - public addWarning = (toastOrTitle: ToastInput) => this.toasts.addWarning(toastOrTitle); - public addDanger = (toastOrTitle: ToastInput) => this.toasts.addDanger(toastOrTitle); - public addError = (error: Error, options: ErrorToastOptions) => - this.toasts.addError(error, options); -} diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index 337fdb80da7e45..f60130d367b584 100644 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -33,6 +33,14 @@ export class KibanaLegacyPlugin { loadFontAwesome: async () => { await import('./font_awesome'); }, + /** + * Loads angular bootstrap modules. Should be removed once the last consumer has migrated to EUI + * @deprecated + */ + loadAngularBootstrap: async () => { + const { initAngularBootstrap } = await import('./angular_bootstrap'); + initAngularBootstrap(); + }, /** * @deprecated * Just exported for wiring up with dashboard mode, should not be used. diff --git a/src/plugins/kibana_legacy/public/utils/index.ts b/src/plugins/kibana_legacy/public/utils/index.ts index db3c0af6c8cb94..94233558b4627c 100644 --- a/src/plugins/kibana_legacy/public/utils/index.ts +++ b/src/plugins/kibana_legacy/public/utils/index.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -export * from './system_api'; // @ts-ignore export { KbnAccessibleClickProvider } from './kbn_accessible_click'; // @ts-ignore diff --git a/src/plugins/kibana_legacy/public/utils/system_api.ts b/src/plugins/kibana_legacy/public/utils/system_api.ts deleted file mode 100644 index d0fe221935ba51..00000000000000 --- a/src/plugins/kibana_legacy/public/utils/system_api.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { IRequestConfig } from 'angular'; - -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 - * @param originalHeaders Object representing set of headers - * @return Object representing set of headers, with system API header added in - */ -export function addSystemApiHeader(originalHeaders: Record) { - const systemApiHeaders = { - [SYSTEM_REQUEST_HEADER_NAME]: true, - }; - return { - ...originalHeaders, - ...systemApiHeaders, - }; -} - -/** - * Returns true if request is a system API request; false otherwise - * - * @param request Object Request object created by $http service - * @return true if request is a system API request; false otherwise - */ -export function isSystemApiRequest(request: IRequestConfig) { - const { headers } = request; - return ( - headers && (!!headers[SYSTEM_REQUEST_HEADER_NAME] || !!headers[LEGACY_SYSTEM_API_HEADER_NAME]) - ); -} diff --git a/src/plugins/timelion/public/plugin.ts b/src/plugins/timelion/public/plugin.ts index 6f8dbfdcc67041..63ea9a38e2795d 100644 --- a/src/plugins/timelion/public/plugin.ts +++ b/src/plugins/timelion/public/plugin.ts @@ -19,7 +19,7 @@ import { AppNavLinkStatus, } from '../../../core/public'; import { Panel } from './panels/panel'; -import { initAngularBootstrap } from '../../kibana_legacy/public'; +import { KibanaLegacyStart } from '../../kibana_legacy/public'; import { createKbnUrlTracker } from '../../kibana_utils/public'; import { DataPublicPluginStart, esFilters, DataPublicPluginSetup } from '../../data/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; @@ -41,6 +41,7 @@ export interface TimelionPluginStartDependencies { visualizations: VisualizationsStart; visTypeTimelion: VisTypeTimelionPluginStart; savedObjects: SavedObjectsStart; + kibanaLegacy: KibanaLegacyStart; } /** @internal */ @@ -91,7 +92,6 @@ export class TimelionPlugin stopUrlTracker(); }; - initAngularBootstrap(); core.application.register({ id: 'timelion', title: 'Timelion', @@ -103,6 +103,7 @@ export class TimelionPlugin visTypeTimelion.isUiEnabled === false ? AppNavLinkStatus.hidden : AppNavLinkStatus.default, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); + await pluginsStart.kibanaLegacy.loadAngularBootstrap(); this.currentHistory = params.history; appMounted(); diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js index 65e26ddf6e03fd..cbc3db6585a7da 100644 --- a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js +++ b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js @@ -15,7 +15,7 @@ import { round } from 'lodash'; import { getFieldFormatsRegistry } from '../../../../data/public/test_utils'; import { coreMock } from '../../../../../core/public/mocks'; -import { initAngularBootstrap } from '../../../../kibana_legacy/public'; +import { initAngularBootstrap } from '../../../../kibana_legacy/public/angular_bootstrap'; import { setUiSettings } from '../../../../data/public/services'; import { UI_SETTINGS } from '../../../../data/public/'; import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../../share/public'; @@ -60,10 +60,12 @@ describe('Table Vis - AggTable Directive', function () { initTableVisLegacyModule(tableVisModule); }; + beforeAll(async () => { + await initAngularBootstrap(); + }); beforeEach(() => { setUiSettings(core.uiSettings); setFormatService(getFieldFormatsRegistry(core)); - initAngularBootstrap(); initLocalAngular(); angular.mock.module('kibana/table_vis'); angular.mock.inject(($injector, config) => { diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js index 1c6630e30e5f73..ba04b2f449f6dd 100644 --- a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js +++ b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js @@ -13,11 +13,11 @@ import expect from '@kbn/expect'; import { getFieldFormatsRegistry } from '../../../../data/public/test_utils'; import { coreMock } from '../../../../../core/public/mocks'; -import { initAngularBootstrap } from '../../../../kibana_legacy/public'; import { setUiSettings } from '../../../../data/public/services'; import { setFormatService } from '../../services'; import { getInnerAngular } from '../get_inner_angular'; import { initTableVisLegacyModule } from '../table_vis_legacy_module'; +import { initAngularBootstrap } from '../../../../kibana_legacy/public/angular_bootstrap'; import { tabifiedData } from './tabified_data'; const uiSettings = new Map(); @@ -40,10 +40,12 @@ describe('Table Vis - AggTableGroup Directive', function () { initTableVisLegacyModule(tableVisModule); }; + beforeAll(async () => { + await initAngularBootstrap(); + }); beforeEach(() => { setUiSettings(core.uiSettings); setFormatService(getFieldFormatsRegistry(core)); - initAngularBootstrap(); initLocalAngular(); angular.mock.module('kibana/table_vis'); angular.mock.inject(($injector) => { diff --git a/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts b/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts index 09fde318ee4dff..412dd904a5e872 100644 --- a/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts +++ b/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts @@ -16,7 +16,6 @@ import 'angular-recursion'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { CoreStart, IUiSettingsClient, PluginInitializerContext } from 'kibana/public'; import { - initAngularBootstrap, PaginateDirectiveProvider, PaginateControlsDirectiveProvider, PrivateProvider, @@ -24,8 +23,6 @@ import { KbnAccessibleClickProvider, } from '../../../kibana_legacy/public'; -initAngularBootstrap(); - const thirdPartyAngularDependencies = ['ngSanitize', 'ui.bootstrap', 'RecursionHelper']; export function getAngularModule(name: string, core: CoreStart, context: PluginInitializerContext) { diff --git a/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts index 77148803e7978b..3feff52f86792b 100644 --- a/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts +++ b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts @@ -12,6 +12,7 @@ import $ from 'jquery'; import 'angular-sanitize'; import 'angular-mocks'; +import { initAngularBootstrap } from '../../../../kibana_legacy/public/angular_bootstrap'; import { getAngularModule } from '../get_inner_angular'; import { initTableVisLegacyModule } from '../table_vis_legacy_module'; import { coreMock } from '../../../../../core/public/mocks'; @@ -56,6 +57,10 @@ describe('Table Vis - Paginated table', () => { const defaultPerPage = 10; let paginatedTable: any; + beforeAll(async () => { + await initAngularBootstrap(); + }); + const initLocalAngular = () => { const tableVisModule = getAngularModule( 'kibana/table_vis', diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_controller.test.ts b/src/plugins/vis_type_table/public/legacy/table_vis_controller.test.ts index 36a9cc9cce77fe..f4a742ea16cb4f 100644 --- a/src/plugins/vis_type_table/public/legacy/table_vis_controller.test.ts +++ b/src/plugins/vis_type_table/public/legacy/table_vis_controller.test.ts @@ -13,6 +13,7 @@ import $ from 'jquery'; import { getAngularModule } from './get_inner_angular'; import { initTableVisLegacyModule } from './table_vis_legacy_module'; +import { initAngularBootstrap } from '../../../kibana_legacy/public/angular_bootstrap'; import { tableVisLegacyTypeDefinition } from './table_vis_legacy_type'; import { Vis } from '../../../visualizations/public'; import { stubFields } from '../../../data/public/stubs'; @@ -76,6 +77,9 @@ describe('Table Vis - Controller', () => { initTableVisLegacyModule(tableVisModule); }; + beforeAll(async () => { + await initAngularBootstrap(); + }); beforeEach(initLocalAngular); beforeEach(angular.mock.module('kibana/table_vis')); diff --git a/src/plugins/vis_type_table/public/legacy/vis_controller.ts b/src/plugins/vis_type_table/public/legacy/vis_controller.ts index ee446c58c00139..ec198aa96f1f96 100644 --- a/src/plugins/vis_type_table/public/legacy/vis_controller.ts +++ b/src/plugins/vis_type_table/public/legacy/vis_controller.ts @@ -56,6 +56,7 @@ export function getTableVisualizationControllerClass( async initLocalAngular() { if (!this.tableVisModule) { const [coreStart, { kibanaLegacy }] = await core.getStartServices(); + await kibanaLegacy.loadAngularBootstrap(); this.tableVisModule = getAngularModule(innerAngularName, coreStart, context); initTableVisLegacyModule(this.tableVisModule); kibanaLegacy.loadFontAwesome(); diff --git a/x-pack/plugins/graph/public/plugin.ts b/x-pack/plugins/graph/public/plugin.ts index 4525b42b3feb45..ec19e639b91c96 100644 --- a/x-pack/plugins/graph/public/plugin.ts +++ b/x-pack/plugins/graph/public/plugin.ts @@ -19,10 +19,7 @@ import { } from '../../../../src/core/public'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; -import { - initAngularBootstrap, - KibanaLegacyStart, -} from '../../../../src/plugins/kibana_legacy/public'; +import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../src/plugins/navigation/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; @@ -77,7 +74,6 @@ export class GraphPlugin const config = this.initializerContext.config.get(); - initAngularBootstrap(); core.application.register({ id: 'graph', title: 'Graph', @@ -88,6 +84,7 @@ export class GraphPlugin updater$: this.appUpdater$, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); + await pluginsStart.kibanaLegacy.loadAngularBootstrap(); coreStart.chrome.docTitle.change( i18n.translate('xpack.graph.pageTitle', { defaultMessage: 'Graph' }) ); diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index a5b7d4906b5869..9f84165a27ba9f 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -93,7 +93,10 @@ export class MonitoringPlugin category: DEFAULT_APP_CATEGORIES.management, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); - const { AngularApp } = await import('./angular'); + const [, { AngularApp }] = await Promise.all([ + pluginsStart.kibanaLegacy.loadAngularBootstrap(), + import('./angular'), + ]); const deps: MonitoringStartPluginDependencies = { navigation: pluginsStart.navigation, kibanaLegacy: pluginsStart.kibanaLegacy,