From c24318ae407376a2528fcf6339e67fcc36f7a8c3 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 29 Jun 2021 13:53:56 -0600 Subject: [PATCH 001/121] [RAC] [Cases] All cases table column design updates (#103544) --- .../public/components/all_cases/actions.tsx | 85 ++---------- .../all_cases/all_cases_generic.test.tsx | 18 +++ .../all_cases/all_cases_generic.tsx | 1 + .../components/all_cases/columns.test.tsx | 19 +++ .../public/components/all_cases/columns.tsx | 108 ++++++++------- .../components/all_cases/index.test.tsx | 124 +++++------------- .../components/all_cases/translations.ts | 13 +- .../case_action_bar/status_context_menu.tsx | 24 ++-- .../components/edit_connector/index.test.tsx | 5 + .../components/edit_connector/index.tsx | 14 +- .../plugins/cases/public/containers/mock.ts | 12 ++ .../server/scripts/mock/case/post_case.json | 3 +- .../observability_security.ts | 2 + 13 files changed, 194 insertions(+), 234 deletions(-) diff --git a/x-pack/plugins/cases/public/components/all_cases/actions.tsx b/x-pack/plugins/cases/public/components/all_cases/actions.tsx index 4820b10308934f..d9e0e0ef025c80 100644 --- a/x-pack/plugins/cases/public/components/all_cases/actions.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/actions.tsx @@ -5,87 +5,24 @@ * 2.0. */ -import { Dispatch } from 'react'; import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; -import { CaseStatuses } from '../../../common'; -import { Case, SubCase } from '../../containers/types'; -import { UpdateCase } from '../../containers/use_get_cases'; -import { statuses } from '../status'; +import { Case } from '../../containers/types'; import * as i18n from './translations'; -import { isIndividual } from './helpers'; interface GetActions { - dispatchUpdate: Dispatch>; deleteCaseOnClick: (deleteCase: Case) => void; } export const getActions = ({ - dispatchUpdate, deleteCaseOnClick, -}: GetActions): Array> => { - const openCaseAction = { - available: (item: Case | SubCase) => item.status !== CaseStatuses.open, - enabled: (item: Case | SubCase) => isIndividual(item), - description: statuses[CaseStatuses.open].actions.single.title, - icon: statuses[CaseStatuses.open].icon, - name: statuses[CaseStatuses.open].actions.single.title, - onClick: (theCase: Case) => - dispatchUpdate({ - updateKey: 'status', - updateValue: CaseStatuses.open, - caseId: theCase.id, - version: theCase.version, - }), - type: 'icon' as const, - 'data-test-subj': 'action-open', - }; - - const makeInProgressAction = { - available: (item: Case) => item.status !== CaseStatuses['in-progress'], - enabled: (item: Case | SubCase) => isIndividual(item), - description: statuses[CaseStatuses['in-progress']].actions.single.title, - icon: statuses[CaseStatuses['in-progress']].icon, - name: statuses[CaseStatuses['in-progress']].actions.single.title, - onClick: (theCase: Case) => - dispatchUpdate({ - updateKey: 'status', - updateValue: CaseStatuses['in-progress'], - caseId: theCase.id, - version: theCase.version, - }), - type: 'icon' as const, - 'data-test-subj': 'action-in-progress', - }; - - const closeCaseAction = { - available: (item: Case | SubCase) => item.status !== CaseStatuses.closed, - enabled: (item: Case | SubCase) => isIndividual(item), - description: statuses[CaseStatuses.closed].actions.single.title, - icon: statuses[CaseStatuses.closed].icon, - name: statuses[CaseStatuses.closed].actions.single.title, - onClick: (theCase: Case) => - dispatchUpdate({ - updateKey: 'status', - updateValue: CaseStatuses.closed, - caseId: theCase.id, - version: theCase.version, - }), - type: 'icon' as const, - 'data-test-subj': 'action-close', - }; - - return [ - openCaseAction, - makeInProgressAction, - closeCaseAction, - { - description: i18n.DELETE_CASE(), - icon: 'trash', - name: i18n.DELETE_CASE(), - onClick: deleteCaseOnClick, - type: 'icon', - 'data-test-subj': 'action-delete', - }, - ]; -}; +}: GetActions): Array> => [ + { + description: i18n.DELETE_CASE(), + icon: 'trash', + name: i18n.DELETE_CASE(), + onClick: deleteCaseOnClick, + type: 'icon', + 'data-test-subj': 'action-delete', + }, +]; diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.test.tsx index a0a5bb08ef7703..47c683becb244a 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.test.tsx @@ -34,6 +34,24 @@ const alertDataMock = { alertId: 'alert-id', owner: SECURITY_SOLUTION_OWNER, }; +jest.mock('../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../common/lib/kibana'); + return { + ...originalModule, + useKibana: () => ({ + services: { + triggersActionsUi: { + actionTypeRegistry: { + get: jest.fn().mockReturnValue({ + actionTypeTitle: '.jira', + iconClass: 'logoSecurity', + }), + }, + }, + }, + }), + }; +}); describe('AllCasesGeneric ', () => { beforeEach(() => { 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 308c2186b52ed2..a6d8afc3b8b230 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 @@ -199,6 +199,7 @@ export const AllCasesGeneric = React.memo( isLoadingCases: loading, refreshCases, showActions, + userCanCrud, }); const itemIdToExpandedRowMap = useMemo( diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.test.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.test.tsx index c7a255da9dda68..0f0189f2d29c27 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.test.tsx @@ -13,6 +13,25 @@ import { ExternalServiceColumn } from './columns'; import { useGetCasesMockState } from '../../containers/mock'; +jest.mock('../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../common/lib/kibana'); + return { + ...originalModule, + useKibana: () => ({ + services: { + triggersActionsUi: { + actionTypeRegistry: { + get: jest.fn().mockReturnValue({ + actionTypeTitle: '.jira', + iconClass: 'logoSecurity', + }), + }, + }, + }, + }), + }; +}); + describe('ExternalServiceColumn ', () => { it('Not pushed render', () => { const wrapper = mount( diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index a5a299851d975a..ad4447223837c2 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -16,6 +16,7 @@ import { EuiTableFieldDataColumnType, EuiFlexGroup, EuiFlexItem, + EuiIcon, } from '@elastic/eui'; import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import styled from 'styled-components'; @@ -25,13 +26,14 @@ import { getEmptyTagValue } from '../empty_value'; import { FormattedRelativePreferenceDate } from '../formatted_date'; import { CaseDetailsHrefSchema, CaseDetailsLink, CasesNavigation } from '../links'; import * as i18n from './translations'; -import { Status } from '../status'; import { getSubCasesStatusCountsBadges, isSubCase } from './helpers'; import { ALERTS } from '../../common/translations'; import { getActions } from './actions'; import { UpdateCase } from '../../containers/use_get_cases'; import { useDeleteCases } from '../../containers/use_delete_cases'; import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; +import { useKibana } from '../../common/lib/kibana'; +import { StatusContextMenu } from '../case_action_bar/status_context_menu'; export type CasesColumns = | EuiTableActionsColumnType @@ -62,6 +64,7 @@ export interface GetCasesColumn { isLoadingCases: string[]; refreshCases?: (a?: boolean) => void; showActions: boolean; + userCanCrud: boolean; } export const useCasesColumns = ({ caseDetailsNavigation, @@ -72,6 +75,7 @@ export const useCasesColumns = ({ isLoadingCases, refreshCases, showActions, + userCanCrud, }: GetCasesColumn): CasesColumns[] => { // Delete case const { @@ -113,9 +117,8 @@ export const useCasesColumns = ({ () => getActions({ deleteCaseOnClick: toggleDeleteModal, - dispatchUpdate: handleDispatchUpdate, }), - [toggleDeleteModal, handleDispatchUpdate] + [toggleDeleteModal] ); useEffect(() => { @@ -267,18 +270,6 @@ export const useCasesColumns = ({ return getEmptyTagValue(); }, }, - { - name: i18n.INCIDENT_MANAGEMENT_SYSTEM, - render: (theCase: Case) => { - if (theCase.externalService != null) { - return renderStringField( - `${theCase.externalService.connectorName}`, - `case-table-column-connector` - ); - } - return getEmptyTagValue(); - }, - }, { name: i18n.STATUS, render: (theCase: Case) => { @@ -286,7 +277,20 @@ export const useCasesColumns = ({ if (theCase.status == null || theCase.type === CaseType.collection) { return getEmptyTagValue(); } - return ; + return ( + 0} + onStatusChanged={(status) => + handleDispatchUpdate({ + updateKey: 'status', + updateValue: status, + caseId: theCase.id, + version: theCase.version, + }) + } + /> + ); } const badges = getSubCasesStatusCountsBadges(theCase.subCases); @@ -322,36 +326,48 @@ interface Props { theCase: Case; } +const IconWrapper = styled.span` + svg { + height: 20px !important; + position: relative; + top: 3px; + width: 20px !important; + } +`; export const ExternalServiceColumn: React.FC = ({ theCase }) => { - const handleRenderDataToPush = useCallback(() => { - const lastCaseUpdate = theCase.updatedAt != null ? new Date(theCase.updatedAt) : null; - const lastCasePush = - theCase.externalService?.pushedAt != null - ? new Date(theCase.externalService?.pushedAt) - : null; - const hasDataToPush = - lastCasePush === null || - (lastCasePush != null && - lastCaseUpdate != null && - lastCasePush.getTime() < lastCaseUpdate?.getTime()); - return ( -

- - {theCase.externalService?.externalTitle} - - {hasDataToPush - ? renderStringField(i18n.REQUIRES_UPDATE, `case-table-column-external-requiresUpdate`) - : renderStringField(i18n.UP_TO_DATE, `case-table-column-external-upToDate`)} -

- ); - }, [theCase]); - if (theCase.externalService !== null) { - return handleRenderDataToPush(); + const { triggersActionsUi } = useKibana().services; + + if (theCase.externalService == null) { + return renderStringField(i18n.NOT_PUSHED, `case-table-column-external-notPushed`); } - return renderStringField(i18n.NOT_PUSHED, `case-table-column-external-notPushed`); + + const lastCaseUpdate = theCase.updatedAt != null ? new Date(theCase.updatedAt) : null; + const lastCasePush = + theCase.externalService?.pushedAt != null ? new Date(theCase.externalService?.pushedAt) : null; + const hasDataToPush = + lastCasePush === null || + (lastCaseUpdate != null && lastCasePush.getTime() < lastCaseUpdate?.getTime()); + return ( +

+ + + + + {theCase.externalService?.externalTitle} + + {hasDataToPush + ? renderStringField(i18n.REQUIRES_UPDATE, `case-table-column-external-requiresUpdate`) + : renderStringField(i18n.UP_TO_DATE, `case-table-column-external-upToDate`)} +

+ ); }; diff --git a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx index c3c0f0bf075dd1..4c15550f3ce3c6 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx @@ -36,7 +36,24 @@ const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; const useUpdateCasesMock = useUpdateCases as jest.Mock; const useGetActionLicenseMock = useGetActionLicense as jest.Mock; -jest.mock('../../common/lib/kibana'); +jest.mock('../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../common/lib/kibana'); + return { + ...originalModule, + useKibana: () => ({ + services: { + triggersActionsUi: { + actionTypeRegistry: { + get: jest.fn().mockReturnValue({ + actionTypeTitle: '.jira', + iconClass: 'logoSecurity', + }), + }, + }, + }, + }), + }; +}); describe('AllCasesGeneric', () => { const defaultAllCasesProps: AllCasesProps = { @@ -119,6 +136,7 @@ describe('AllCasesGeneric', () => { handleIsLoading: jest.fn(), isLoadingCases: [], showActions: true, + userCanCrud: true, }; beforeEach(() => { @@ -274,7 +292,7 @@ describe('AllCasesGeneric', () => { }); }); - it('should render correct actions for case (with type individual and filter status open)', async () => { + it('should render delete actions for case', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open }, @@ -284,18 +302,12 @@ describe('AllCasesGeneric', () => { ); - wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); await waitFor(() => { - expect(wrapper.find('[data-test-subj="action-open"]').exists()).toBeFalsy(); - expect( - wrapper.find('[data-test-subj="action-in-progress"]').first().props().disabled - ).toBeFalsy(); - expect(wrapper.find('[data-test-subj="action-close"]').first().props().disabled).toBeFalsy(); expect(wrapper.find('[data-test-subj="action-delete"]').first().props().disabled).toBeFalsy(); }); }); - it('should enable correct actions for sub cases', async () => { + it.skip('should enable correct actions for sub cases', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, data: { @@ -327,25 +339,9 @@ describe('AllCasesGeneric', () => { ); - wrapper - .find( - '[data-test-subj="sub-cases-table-my-case-with-subcases"] [data-test-subj="euiCollapsedItemActionsButton"]' - ) - .last() - .simulate('click'); - - await waitFor(() => { - expect(wrapper.find('[data-test-subj="action-open"]').first().props().disabled).toEqual(true); - expect( - wrapper.find('[data-test-subj="action-in-progress"]').first().props().disabled - ).toEqual(true); - expect(wrapper.find('[data-test-subj="action-close"]').first().props().disabled).toEqual( - true - ); - expect(wrapper.find('[data-test-subj="action-delete"]').first().props().disabled).toEqual( - false - ); - }); + expect(wrapper.find('[data-test-subj="action-delete"]').first().props().disabled).toEqual( + false + ); }); it('should not render case link when caseDetailsNavigation is not passed or actions on showActions=false', async () => { @@ -362,6 +358,7 @@ describe('AllCasesGeneric', () => { filterStatus: CaseStatuses.open, handleIsLoading: jest.fn(), showActions: false, + userCanCrud: true, }) ); await waitFor(() => { @@ -387,14 +384,17 @@ describe('AllCasesGeneric', () => { }); }); - it('closes case when row action icon clicked', async () => { + it('Updates status when status context menu is updated', async () => { const wrapper = mount( ); - wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); - wrapper.find('[data-test-subj="action-close"]').first().simulate('click'); + wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).first().simulate('click'); + wrapper + .find(`[data-test-subj="case-view-status-dropdown-closed"] button`) + .first() + .simulate('click'); await waitFor(() => { const firstCase = useGetCasesMockState.data.cases[0]; @@ -409,66 +409,6 @@ describe('AllCasesGeneric', () => { }); }); - it('opens case when row action icon clicked', async () => { - useGetCasesMock.mockReturnValue({ - ...defaultGetCases, - data: { - ...defaultGetCases.data, - cases: [ - { - ...defaultGetCases.data.cases[0], - status: CaseStatuses.closed, - }, - ], - }, - filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.closed }, - }); - - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); - wrapper.find('[data-test-subj="action-open"]').first().simulate('click'); - - await waitFor(() => { - const firstCase = useGetCasesMockState.data.cases[0]; - expect(dispatchUpdateCaseProperty.mock.calls[0][0]).toEqual( - expect.objectContaining({ - caseId: firstCase.id, - updateKey: 'status', - updateValue: CaseStatuses.open, - version: firstCase.version, - }) - ); - }); - }); - - it('put case in progress when row action icon clicked', async () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); - wrapper.find('[data-test-subj="action-in-progress"]').first().simulate('click'); - - await waitFor(() => { - const firstCase = useGetCasesMockState.data.cases[0]; - expect(dispatchUpdateCaseProperty.mock.calls[0][0]).toEqual( - expect.objectContaining({ - caseId: firstCase.id, - updateKey: 'status', - updateValue: CaseStatuses['in-progress'], - version: firstCase.version, - }) - ); - }); - }); - it('Bulk delete', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, @@ -794,7 +734,7 @@ describe('AllCasesGeneric', () => { closedAt: null, closedBy: null, comments: [], - connector: { fields: null, id: 'none', name: 'My Connector', type: '.none' }, + connector: { fields: null, id: '123', name: 'My Connector', type: '.jira' }, createdAt: '2020-02-19T23:06:33.798Z', createdBy: { email: 'leslie.knope@elastic.co', diff --git a/x-pack/plugins/cases/public/components/all_cases/translations.ts b/x-pack/plugins/cases/public/components/all_cases/translations.ts index 8da90f32fabdf2..be1aa256db657a 100644 --- a/x-pack/plugins/cases/public/components/all_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/all_cases/translations.ts @@ -90,10 +90,15 @@ export const REFRESH = i18n.translate('xpack.cases.caseTable.refreshTitle', { defaultMessage: 'Refresh', }); -export const SERVICENOW_LINK_ARIA = i18n.translate('xpack.cases.caseTable.serviceNowLinkAria', { - defaultMessage: 'click to view the incident on servicenow', -}); - +export const PUSH_LINK_ARIA = (thirdPartyName: string): string => + i18n.translate('xpack.cases.caseTable.pushLinkAria', { + values: { thirdPartyName }, + defaultMessage: 'click to view the incident on { thirdPartyName }.', + }); export const STATUS = i18n.translate('xpack.cases.caseTable.status', { defaultMessage: 'Status', }); + +export const CHANGE_STATUS = i18n.translate('xpack.cases.caseTable.changeStatus', { + defaultMessage: 'Change status', +}); diff --git a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx index 3ee7ab65902154..603efb253f0517 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx @@ -9,6 +9,7 @@ import React, { memo, useCallback, useMemo, useState } from 'react'; import { EuiPopover, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; import { caseStatuses, CaseStatuses } from '../../../common'; import { Status } from '../status'; +import { CHANGE_STATUS } from '../all_cases/translations'; interface Props { currentStatus: CaseStatuses; @@ -53,18 +54,17 @@ const StatusContextMenuComponent: React.FC = ({ ); return ( - <> - - - - + + + ); }; diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx index 29d64afe3284f7..fb45bf6ac3ae0b 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx @@ -67,6 +67,7 @@ describe('EditConnector ', () => { ); + expect(wrapper.find(`[data-test-subj="has-data-to-push-button"]`).exists()).toBeTruthy(); wrapper.find('[data-test-subj="connector-edit"] button').simulate('click'); expect( @@ -173,6 +174,8 @@ describe('EditConnector ', () => { await waitFor(() => expect(wrapper.find(`[data-test-subj="connector-edit"]`).exists()).toBeFalsy() ); + + expect(wrapper.find(`[data-test-subj="has-data-to-push-button"]`).exists()).toBeFalsy(); }); it('displays the permissions error message when one is provided', async () => { @@ -191,6 +194,8 @@ describe('EditConnector ', () => { expect( wrapper.find(`[data-test-subj="edit-connector-no-connectors-msg"]`).exists() ).toBeFalsy(); + + expect(wrapper.find(`[data-test-subj="has-data-to-push-button"]`).exists()).toBeFalsy(); }); }); diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx index fe92bd28ce21c3..0a20d2f5c83037 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx @@ -330,11 +330,15 @@ export const EditConnector = React.memo( )} - {pushCallouts == null && !isLoading && !editConnector && ( - - {pushButton} - - )} + {pushCallouts == null && + !isLoading && + !editConnector && + userCanCrud && + !permissionsError && ( + + {pushButton} + + )} ); diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index df03311005bdb1..a900010235c9f1 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -176,6 +176,12 @@ export const basicPush = { export const pushedCase: Case = { ...basicCase, + connector: { + id: '123', + name: 'My Connector', + type: ConnectorTypes.jira, + fields: null, + }, externalService: basicPush, }; @@ -286,6 +292,12 @@ export const basicPushSnake = { export const pushedCaseSnake = { ...basicCaseSnake, + connector: { + id: '123', + name: 'My Connector', + type: ConnectorTypes.jira, + fields: null, + }, external_service: basicPushSnake, }; diff --git a/x-pack/plugins/cases/server/scripts/mock/case/post_case.json b/x-pack/plugins/cases/server/scripts/mock/case/post_case.json index bed342dd69fe9d..a6e0e750a6683f 100644 --- a/x-pack/plugins/cases/server/scripts/mock/case/post_case.json +++ b/x-pack/plugins/cases/server/scripts/mock/case/post_case.json @@ -12,5 +12,6 @@ }, "settings": { "syncAlerts": true - } + }, + "owner": "securitySolution" } diff --git a/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts b/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts index f2d78369bafee0..90ccaf7c3df35b 100644 --- a/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts +++ b/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts @@ -75,6 +75,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`doesn't show read-only badge`, async () => { + await PageObjects.common.navigateToActualUrl('observabilityCases'); await PageObjects.observability.expectNoReadOnlyCallout(); }); @@ -142,6 +143,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`shows read-only glasses badge`, async () => { + await PageObjects.common.navigateToActualUrl('observabilityCases'); await PageObjects.observability.expectReadOnlyGlassesBadge(); }); From 21dad7edb575e3543a3d71d0cd1ce0cc771b8279 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 29 Jun 2021 21:07:07 +0100 Subject: [PATCH 002/121] [ML] Update file data visualizer permissions (#101169) * [ML] Update file data visualizer permissions * adding home bundle * fixing translations * removing home from bundles * switching to current user for analysis * adding find structure permission check * clean up * updating text * updating maps * removing has_find_file_structure_permission endpoint * removing more code * adding permission error message * renaming variable * adding fileUpload:analyzeFile back into ML * updating error text * updating snapshots Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../file_data_visualizer_view.js | 34 ++++++++++++++----- .../file_error_callouts.tsx | 23 +++++++++++++ .../__snapshots__/oss_features.test.ts.snap | 2 ++ .../plugins/features/server/oss_features.ts | 1 + x-pack/plugins/file_upload/server/routes.ts | 4 --- x-pack/plugins/maps/server/plugin.ts | 1 - .../plugins/ml/common/types/capabilities.ts | 6 +--- 7 files changed, 53 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js index e36f1040447e71..99b6ef602985f8 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js @@ -15,7 +15,11 @@ import { isEqual } from 'lodash'; import { AboutPanel, LoadingPanel } from '../about_panel'; import { BottomBar } from '../bottom_bar'; import { ResultsView } from '../results_view'; -import { FileCouldNotBeRead, FileTooLarge } from './file_error_callouts'; +import { + FileCouldNotBeRead, + FileTooLarge, + FindFileStructurePermissionDenied, +} from './file_error_callouts'; import { EditFlyout } from '../edit_flyout'; import { ExplanationFlyout } from '../explanation_flyout'; import { ImportView } from '../import_view'; @@ -50,6 +54,7 @@ export class FileDataVisualizerView extends Component { isExplanationFlyoutVisible: false, bottomBarVisible: false, hasPermissionToImport: false, + fileCouldNotBeReadPermissionError: false, }; this.overrides = {}; @@ -87,6 +92,7 @@ export class FileDataVisualizerView extends Component { fileSize: 0, fileTooLarge: false, fileCouldNotBeRead: false, + fileCouldNotBeReadPermissionError: false, serverError: null, results: undefined, explanation: undefined, @@ -182,17 +188,19 @@ export class FileDataVisualizerView extends Component { fileCouldNotBeRead: isRetry, }); } catch (error) { + const fileCouldNotBeReadPermissionError = error.body.statusCode === 403; this.setState({ results: undefined, explanation: undefined, loaded: false, loading: false, fileCouldNotBeRead: true, + fileCouldNotBeReadPermissionError, serverError: error, }); // reload the results with the previous overrides - if (isRetry === false) { + if (isRetry === false && fileCouldNotBeReadPermissionError === false) { this.setState({ loading: true, loaded: false, @@ -275,6 +283,7 @@ export class FileDataVisualizerView extends Component { isExplanationFlyoutVisible, bottomBarVisible, hasPermissionToImport, + fileCouldNotBeReadPermissionError, } = this.state; const fields = @@ -286,7 +295,12 @@ export class FileDataVisualizerView extends Component {
{mode === MODE.READ && ( <> - {!loading && !loaded && } + {!loading && !loaded && ( + + )} {loading && } @@ -296,11 +310,15 @@ export class FileDataVisualizerView extends Component { {fileCouldNotBeRead && loading === false && ( <> - + {fileCouldNotBeReadPermissionError ? ( + + ) : ( + + )} )} diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_error_callouts.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_error_callouts.tsx index 7b6378e34e78e8..62d860c1513e85 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_error_callouts.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_error_callouts.tsx @@ -146,3 +146,26 @@ export const Explanation: FC<{ error: FindFileStructureErrorResponse }> = ({ err ); }; + +export const FindFileStructurePermissionDenied: FC = () => { + return ( + <> + + } + color="danger" + iconType="cross" + data-test-subj="dataVisualizerFileStructurePermissionDeniedErrorCallout" + > + + + + ); +}; diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index e1e5776d87c756..fe5e4fb4f1e0ed 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -671,6 +671,7 @@ Array [ }, }, "api": Array [ + "fileUpload:analyzeFile", "store_search_session", ], "app": Array [ @@ -1199,6 +1200,7 @@ Array [ }, }, "api": Array [ + "fileUpload:analyzeFile", "store_search_session", ], "app": Array [ diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts index d1e96b5a788ec3..f3fd934b4c3e83 100644 --- a/x-pack/plugins/features/server/oss_features.ts +++ b/x-pack/plugins/features/server/oss_features.ts @@ -37,6 +37,7 @@ export const buildOSSFeatures = ({ privileges: { all: { app: ['discover', 'kibana'], + api: ['fileUpload:analyzeFile'], catalogue: ['discover'], savedObject: { all: ['search', 'query', 'index-pattern'], diff --git a/x-pack/plugins/file_upload/server/routes.ts b/x-pack/plugins/file_upload/server/routes.ts index bd3aa2688c735a..c957916e7f3210 100644 --- a/x-pack/plugins/file_upload/server/routes.ts +++ b/x-pack/plugins/file_upload/server/routes.ts @@ -141,7 +141,6 @@ export function fileUploadRoutes(coreSetup: CoreSetup, logge accepts: ['application/json'], maxBytes: MAX_FILE_SIZE_BYTES, }, - tags: ['access:fileUpload:import'], }, }, async (context, request, response) => { @@ -185,9 +184,6 @@ export function fileUploadRoutes(coreSetup: CoreSetup, logge validate: { body: schema.object({ index: schema.string() }), }, - options: { - tags: ['access:fileUpload:import'], - }, }, async (context, request, response) => { try { diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index c5fc602864f965..1119be32a04def 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -185,7 +185,6 @@ export class MapsPlugin implements Plugin { catalogue: [APP_ID], privileges: { all: { - api: ['fileUpload:import'], app: [APP_ID, 'kibana'], catalogue: [APP_ID], savedObject: { diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 3545a85305c178..ef8d35e52a9516 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -105,11 +105,7 @@ export function getPluginPrivileges() { return { admin: { ...privilege, - api: [ - 'fileUpload:import', - 'fileUpload:analyzeFile', - ...allMlCapabilitiesKeys.map((k) => `ml:${k}`), - ], + api: ['fileUpload:analyzeFile', ...allMlCapabilitiesKeys.map((k) => `ml:${k}`)], catalogue: [PLUGIN_ID, `${PLUGIN_ID}_file_data_visualizer`], ui: allMlCapabilitiesKeys, savedObject: { From fea63a2d36727838fa6ce335fbbb1c3eb2282505 Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Tue, 29 Jun 2021 22:08:15 +0200 Subject: [PATCH 003/121] [Security Solution][Endpoint] Include actions and responses for endpoints only (#103159) --- .../data_generators/fleet_action_generator.ts | 2 +- .../common/endpoint/index_data.ts | 6 +- .../pages/endpoint_hosts/store/middleware.ts | 6 +- .../endpoint/routes/actions/audit_log.test.ts | 12 +- .../server/endpoint/routes/actions/service.ts | 154 +++++++++++------- 5 files changed, 104 insertions(+), 76 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts index 6cc5ab7f084476..22d81ba4a34580 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts @@ -43,7 +43,7 @@ export class FleetActionGenerator extends BaseDataGenerator { return merge(this.generate({ data: { command: 'unisolate' } }), overrides); } - /** Generates an action response */ + /** Generates an endpoint action response */ generateResponse(overrides: DeepPartial = {}): EndpointActionResponse { const timeStamp = new Date(); diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index 959db0d964aaec..66a3efa82347ec 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -421,7 +421,7 @@ const indexFleetActionsForHost = async ( action.agents = [agentId]; - await esClient.index( + esClient.index( { index: AGENT_ACTIONS_INDEX, body: action, @@ -436,7 +436,7 @@ const indexFleetActionsForHost = async ( action_data: action.data, }); - await esClient.index( + esClient.index( { index: AGENT_ACTIONS_RESULTS_INDEX, body: actionResponse, @@ -449,7 +449,7 @@ const indexFleetActionsForHost = async ( if (fleetActionGenerator.randomFloat() < 0.3) { const randomFloat = fleetActionGenerator.randomFloat(); - // 60% of the time just add either an Isoalte -OR- an UnIsolate action + // 60% of the time just add either an Isolate -OR- an UnIsolate action if (randomFloat < 0.6) { let action: EndpointAction; 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 53b30aeb02bd53..ffeef3f7bf5e18 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 @@ -412,9 +412,11 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory + new Date(b.item.data['@timestamp']) > new Date(a.item.data['@timestamp']) ? 1 : -1 + ); const updatedLogData = { page: activityLog.page, 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 9b737217753824..c7f07151f87246 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 @@ -157,20 +157,14 @@ describe('Action Log API', () => { it('should have actions and action responses', async () => { havingActionsAndResponses( - [ - aMockAction().withAgent(mockID).withAction('isolate'), - aMockAction().withAgent(mockID).withAction('unisolate'), - aMockAction().withAgent(mockID).withAction('isolate'), - ], - [aMockResponse(actionID, mockID), aMockResponse(actionID, mockID)] + [aMockAction().withAgent(mockID).withAction('isolate').withID(actionID)], + [aMockResponse(actionID, mockID)] ); const response = await getActivityLog(); const responseBody = response.ok.mock.calls[0][0]?.body as ActivityLog; expect(response.ok).toBeCalled(); - expect(responseBody.data).toHaveLength(5); - expect(responseBody.data.filter((x: any) => x.type === 'response')).toHaveLength(2); - expect(responseBody.data.filter((x: any) => x.type === 'action')).toHaveLength(3); + expect(responseBody.data).toHaveLength(2); }); it('should throw errors when no results for some agentID', async () => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts index 1a8b17bf19e18f..7a82a56b1f19bc 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts @@ -5,43 +5,10 @@ * 2.0. */ -import { Logger } from 'kibana/server'; -import type { estypes } from '@elastic/elasticsearch'; +import { ElasticsearchClient, Logger } from 'kibana/server'; import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../../../fleet/common'; import { SecuritySolutionRequestHandlerContext } from '../../../types'; - -export const getAuditLogESQuery = ({ - elasticAgentId, - from, - size, -}: { - elasticAgentId: string; - from: number; - size: number; -}): estypes.SearchRequest => { - return { - index: [AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX], - size, - from, - body: { - query: { - bool: { - should: [ - { terms: { agents: [elasticAgentId] } }, - { terms: { agent_id: [elasticAgentId] } }, - ], - }, - }, - sort: [ - { - '@timestamp': { - order: 'desc', - }, - }, - ], - }, - }; -}; +import { ActivityLog, EndpointAction } from '../../../../common/endpoint/types'; export const getAuditLogResponse = async ({ elasticAgentId, @@ -58,48 +25,113 @@ export const getAuditLogResponse = async ({ }): Promise<{ page: number; pageSize: number; - data: Array<{ - type: 'action' | 'response'; - item: { - id: string; - data: unknown; - }; - }>; + data: ActivityLog['data']; }> => { - const size = pageSize; - const from = page <= 1 ? 0 : page * pageSize - pageSize + 1; + 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 }); + + return { + page, + pageSize, + data, + }; +}; +const getActivityLog = async ({ + esClient, + size, + from, + elasticAgentId, + logger, +}: { + esClient: ElasticsearchClient; + elasticAgentId: string; + size: number; + from: number; + logger: Logger; +}) => { const options = { headers: { 'X-elastic-product-origin': 'fleet', }, ignore: [404], }; - const esClient = context.core.elasticsearch.client.asCurrentUser; - let result; - const params = getAuditLogESQuery({ - elasticAgentId, - from, - size, - }); + + let actionsResult; + let responsesResult; try { - result = await esClient.search(params, options); + actionsResult = await esClient.search( + { + index: AGENT_ACTIONS_INDEX, + size, + from, + body: { + query: { + bool: { + filter: [ + { term: { agents: elasticAgentId } }, + { term: { input_type: 'endpoint' } }, + { term: { type: 'INPUT_ACTION' } }, + ], + }, + }, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + }, + }, + options + ); + const actionIds = actionsResult?.body?.hits?.hits?.map( + (e) => (e._source as EndpointAction).action_id + ); + + responsesResult = await esClient.search( + { + index: AGENT_ACTIONS_RESULTS_INDEX, + size: 1000, + body: { + query: { + bool: { + filter: [{ term: { agent_id: elasticAgentId } }, { terms: { action_id: actionIds } }], + }, + }, + }, + }, + options + ); } catch (error) { logger.error(error); throw error; } - if (result?.statusCode !== 200) { + if (actionsResult?.statusCode !== 200) { logger.error(`Error fetching actions log for agent_id ${elasticAgentId}`); throw new Error(`Error fetching actions log for agent_id ${elasticAgentId}`); } - return { - page, - pageSize, - data: result.body.hits.hits.map((e) => ({ - type: e._index.startsWith('.fleet-actions') ? 'action' : 'response', - item: { id: e._id, data: e._source }, - })), - }; + const responses = responsesResult?.body?.hits?.hits?.length + ? responsesResult?.body?.hits?.hits?.map((e) => ({ + type: 'response', + item: { id: e._id, data: e._source }, + })) + : []; + const actions = actionsResult?.body?.hits?.hits?.length + ? actionsResult?.body?.hits?.hits?.map((e) => ({ + type: 'action', + item: { id: e._id, data: e._source }, + })) + : []; + const sortedData = ([...responses, ...actions] as ActivityLog['data']).sort((a, b) => + new Date(b.item.data['@timestamp']) > new Date(a.item.data['@timestamp']) ? 1 : -1 + ); + + return sortedData; }; From fa92958c74136ad61d9fe9d0d0f8e8571d2f8de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ece=20=C3=96zalp?= Date: Tue, 29 Jun 2021 16:21:18 -0400 Subject: [PATCH 004/121] adds hasBorder to ThreatIntelPanelView (#103726) --- .../components/overview_cti_links/threat_intel_panel_view.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9e8375e2630885..4565c16bc2bf65 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 @@ -133,7 +133,7 @@ export const ThreatIntelPanelView: React.FC = ({ > - + <>{button} From 682d969190f70cff3a753cf786a92600ac0c394f Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 29 Jun 2021 21:22:45 +0100 Subject: [PATCH 005/121] chore(NA): remove unused transient dep declaration from @kbn/ui-shared-deps build file (#103666) --- packages/kbn-ui-shared-deps/BUILD.bazel | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/kbn-ui-shared-deps/BUILD.bazel b/packages/kbn-ui-shared-deps/BUILD.bazel index c04f88a42cce03..9096905a2586be 100644 --- a/packages/kbn-ui-shared-deps/BUILD.bazel +++ b/packages/kbn-ui-shared-deps/BUILD.bazel @@ -50,8 +50,6 @@ SRC_DEPS = [ "@npm//fflate", "@npm//jquery", "@npm//loader-utils", - # TODO: we can remove this once EUI patches the dependencies - "@npm//mdast-util-to-hast", "@npm//mini-css-extract-plugin", "@npm//moment", "@npm//moment-timezone", From b9bbfa3695cac04544c04dfe4ecf14ea7236b963 Mon Sep 17 00:00:00 2001 From: Michael Marcialis Date: Tue, 29 Jun 2021 16:41:45 -0400 Subject: [PATCH 006/121] [Lens] Formula Icon, Button, Height Design Update (#103682) * apply `fullScreenEdit` icon; fix docs button size * fix small viewport height flex issue --- .../definitions/formula/editor/formula.scss | 15 +++++++++------ .../definitions/formula/editor/formula_editor.tsx | 4 +--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss index 14b3fc33efb4e6..d66e19bec8a1c0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula.scss @@ -47,8 +47,16 @@ } .lnsFormula__editorContent { + min-height: 0; position: relative; - height: 201px; + + .lnsIndexPatternDimensionEditor:not(.lnsIndexPatternDimensionEditor-isFullscreen) & { + height: 200px; + } + + .lnsIndexPatternDimensionEditor-isFullscreen & { + flex: 1; + } } .lnsFormula__editorPlaceholder { @@ -62,11 +70,6 @@ pointer-events: none; } -.lnsIndexPatternDimensionEditor-isFullscreen .lnsFormula__editorContent { - flex: 1; - min-height: 201px; -} - .lnsFormula__warningText + .lnsFormula__warningText { margin-top: $euiSizeS; border-top: $euiBorderThin; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx index c83135536343d0..97eaf7604df831 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx @@ -622,7 +622,6 @@ export function FormulaEditor({ - {/* TODO: Replace `bolt` with `fullScreenExit` icon (after latest EUI is deployed). */} { toggleFullscreen(); @@ -630,7 +629,7 @@ export function FormulaEditor({ setIsHelpOpen(!isFullscreen); trackUiEvent('toggle_formula_fullscreen'); }} - iconType={isFullscreen ? 'bolt' : 'fullScreen'} + iconType={isFullscreen ? 'fullScreenExit' : 'fullScreen'} size="xs" color="text" flush="right" @@ -758,7 +757,6 @@ export function FormulaEditor({ }} iconType="documentation" color="text" - size="s" aria-label={i18n.translate( 'xpack.lens.formula.editorHelpInlineShowToolTip', { From ebf9e5df76db6f1e59c9bcf4a6f449abda07a213 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 29 Jun 2021 14:36:18 -0700 Subject: [PATCH 007/121] Use new terms enum API for autocomplete value suggestions (#100174) * Migrate kibana.autocomplete config to data plugin * Fix CI * Fix tests * Use new terms enum API for autocomplete value suggestions * Add tiers to config * Re-introduce terms agg and add config/tests for swapping algorithms * Add data_content and data_cold tiers by default * Fix types * Fix maps test * Update tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/data/config.ts | 15 +++ .../server/autocomplete/terms_agg.test.ts | 89 ++++++++++++++ .../data/server/autocomplete/terms_agg.ts | 106 +++++++++++++++++ .../server/autocomplete/terms_enum.test.ts | 74 ++++++++++++ .../data/server/autocomplete/terms_enum.ts | 62 ++++++++++ .../autocomplete/value_suggestions_route.ts | 112 ++++-------------- .../functional/apps/maps/vector_styling.js | 2 +- 7 files changed, 372 insertions(+), 88 deletions(-) create mode 100644 src/plugins/data/server/autocomplete/terms_agg.test.ts create mode 100644 src/plugins/data/server/autocomplete/terms_agg.ts create mode 100644 src/plugins/data/server/autocomplete/terms_enum.test.ts create mode 100644 src/plugins/data/server/autocomplete/terms_enum.ts diff --git a/src/plugins/data/config.ts b/src/plugins/data/config.ts index 1b7bfbc09ad162..a2b5a568b70ef3 100644 --- a/src/plugins/data/config.ts +++ b/src/plugins/data/config.ts @@ -15,6 +15,21 @@ export const configSchema = schema.object({ }), valueSuggestions: schema.object({ enabled: schema.boolean({ defaultValue: true }), + method: schema.oneOf([schema.literal('terms_enum'), schema.literal('terms_agg')], { + defaultValue: 'terms_enum', + }), + tiers: schema.arrayOf( + schema.oneOf([ + schema.literal('data_content'), + schema.literal('data_hot'), + schema.literal('data_warm'), + schema.literal('data_cold'), + schema.literal('data_frozen'), + ]), + { + defaultValue: ['data_hot', 'data_warm', 'data_content', 'data_cold'], + } + ), terminateAfter: schema.duration({ defaultValue: 100000 }), timeout: schema.duration({ defaultValue: 1000 }), }), diff --git a/src/plugins/data/server/autocomplete/terms_agg.test.ts b/src/plugins/data/server/autocomplete/terms_agg.test.ts new file mode 100644 index 00000000000000..e4652c2c422e22 --- /dev/null +++ b/src/plugins/data/server/autocomplete/terms_agg.test.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { coreMock } from '../../../../core/server/mocks'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; +import { ConfigSchema } from '../../config'; +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { ApiResponse } from '@elastic/elasticsearch'; +import { termsAggSuggestions } from './terms_agg'; +import { SearchResponse } from 'elasticsearch'; +import { duration } from 'moment'; + +let savedObjectsClientMock: jest.Mocked; +let esClientMock: DeeplyMockedKeys; +const configMock = ({ + autocomplete: { + valueSuggestions: { timeout: duration(4513), terminateAfter: duration(98430) }, + }, +} as unknown) as ConfigSchema; +const mockResponse = { + body: { + aggregations: { + suggestions: { + buckets: [{ key: 'whoa' }, { key: 'amazing' }], + }, + }, + }, +} as ApiResponse>; + +describe('terms agg suggestions', () => { + beforeEach(() => { + const requestHandlerContext = coreMock.createRequestHandlerContext(); + savedObjectsClientMock = requestHandlerContext.savedObjects.client; + esClientMock = requestHandlerContext.elasticsearch.client.asCurrentUser; + esClientMock.search.mockResolvedValue(mockResponse); + }); + + it('calls the _search API with a terms agg with the given args', async () => { + const result = await termsAggSuggestions( + configMock, + savedObjectsClientMock, + esClientMock, + 'index', + 'fieldName', + 'query', + [], + { name: 'field_name', type: 'string' } + ); + + const [[args]] = esClientMock.search.mock.calls; + + expect(args).toMatchInlineSnapshot(` + Object { + "body": Object { + "aggs": Object { + "suggestions": Object { + "terms": Object { + "execution_hint": "map", + "field": "field_name", + "include": "query.*", + "shard_size": 10, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [], + }, + }, + "size": 0, + "terminate_after": 98430, + "timeout": "4513ms", + }, + "index": "index", + } + `); + expect(result).toMatchInlineSnapshot(` + Array [ + "whoa", + "amazing", + ] + `); + }); +}); diff --git a/src/plugins/data/server/autocomplete/terms_agg.ts b/src/plugins/data/server/autocomplete/terms_agg.ts new file mode 100644 index 00000000000000..b902bae49898fc --- /dev/null +++ b/src/plugins/data/server/autocomplete/terms_agg.ts @@ -0,0 +1,106 @@ +/* + * 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 { get, map } from 'lodash'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; +import { estypes } from '@elastic/elasticsearch'; +import { ConfigSchema } from '../../config'; +import { IFieldType } from '../../common'; +import { findIndexPatternById, getFieldByName } from '../index_patterns'; +import { shimAbortSignal } from '../search'; + +export async function termsAggSuggestions( + config: ConfigSchema, + savedObjectsClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + index: string, + fieldName: string, + query: string, + filters?: estypes.QueryDslQueryContainer[], + field?: IFieldType, + abortSignal?: AbortSignal +) { + const autocompleteSearchOptions = { + timeout: `${config.autocomplete.valueSuggestions.timeout.asMilliseconds()}ms`, + terminate_after: config.autocomplete.valueSuggestions.terminateAfter.asMilliseconds(), + }; + + if (!field?.name && !field?.type) { + const indexPattern = await findIndexPatternById(savedObjectsClient, index); + + field = indexPattern && getFieldByName(fieldName, indexPattern); + } + + const body = await getBody(autocompleteSearchOptions, field ?? fieldName, query, filters); + + const promise = esClient.search({ index, body }); + const result = await shimAbortSignal(promise, abortSignal); + + const buckets = + get(result.body, 'aggregations.suggestions.buckets') || + get(result.body, 'aggregations.nestedSuggestions.suggestions.buckets'); + + return map(buckets ?? [], 'key'); +} + +async function getBody( + // eslint-disable-next-line @typescript-eslint/naming-convention + { timeout, terminate_after }: Record, + field: IFieldType | string, + query: string, + filters: estypes.QueryDslQueryContainer[] = [] +) { + const isFieldObject = (f: any): f is IFieldType => Boolean(f && f.name); + + // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators + const getEscapedQuery = (q: string = '') => + q.replace(/[.?+*|{}[\]()"\\#@&<>~]/g, (match) => `\\${match}`); + + // Helps ensure that the regex is not evaluated eagerly against the terms dictionary + const executionHint = 'map' as const; + + // We don't care about the accuracy of the counts, just the content of the terms, so this reduces + // the amount of information that needs to be transmitted to the coordinating node + const shardSize = 10; + const body = { + size: 0, + timeout, + terminate_after, + query: { + bool: { + filter: filters, + }, + }, + aggs: { + suggestions: { + terms: { + field: isFieldObject(field) ? field.name : field, + include: `${getEscapedQuery(query)}.*`, + execution_hint: executionHint, + shard_size: shardSize, + }, + }, + }, + }; + + if (isFieldObject(field) && field.subType && field.subType.nested) { + return { + ...body, + aggs: { + nestedSuggestions: { + nested: { + path: field.subType.nested.path, + }, + aggs: body.aggs, + }, + }, + }; + } + + return body; +} diff --git a/src/plugins/data/server/autocomplete/terms_enum.test.ts b/src/plugins/data/server/autocomplete/terms_enum.test.ts new file mode 100644 index 00000000000000..be8f179db29c05 --- /dev/null +++ b/src/plugins/data/server/autocomplete/terms_enum.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { termsEnumSuggestions } from './terms_enum'; +import { coreMock } from '../../../../core/server/mocks'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; +import { ConfigSchema } from '../../config'; +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { ApiResponse } from '@elastic/elasticsearch'; + +let savedObjectsClientMock: jest.Mocked; +let esClientMock: DeeplyMockedKeys; +const configMock = { + autocomplete: { valueSuggestions: { tiers: ['data_hot', 'data_warm', 'data_content'] } }, +} as ConfigSchema; +const mockResponse = { + body: { terms: ['whoa', 'amazing'] }, +}; + +describe('_terms_enum suggestions', () => { + beforeEach(() => { + const requestHandlerContext = coreMock.createRequestHandlerContext(); + savedObjectsClientMock = requestHandlerContext.savedObjects.client; + esClientMock = requestHandlerContext.elasticsearch.client.asCurrentUser; + esClientMock.transport.request.mockResolvedValue((mockResponse as unknown) as ApiResponse); + }); + + it('calls the _terms_enum API with the field, query, filters, and config tiers', async () => { + const result = await termsEnumSuggestions( + configMock, + savedObjectsClientMock, + esClientMock, + 'index', + 'fieldName', + 'query', + [], + { name: 'field_name', type: 'string' } + ); + + const [[args]] = esClientMock.transport.request.mock.calls; + + expect(args).toMatchInlineSnapshot(` + Object { + "body": Object { + "field": "field_name", + "index_filter": Object { + "bool": Object { + "must": Array [ + Object { + "terms": Object { + "_tier": Array [ + "data_hot", + "data_warm", + "data_content", + ], + }, + }, + ], + }, + }, + "string": "query", + }, + "method": "POST", + "path": "/index/_terms_enum", + } + `); + expect(result).toEqual(mockResponse.body.terms); + }); +}); diff --git a/src/plugins/data/server/autocomplete/terms_enum.ts b/src/plugins/data/server/autocomplete/terms_enum.ts new file mode 100644 index 00000000000000..c2452b0a099d04 --- /dev/null +++ b/src/plugins/data/server/autocomplete/terms_enum.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; +import { estypes } from '@elastic/elasticsearch'; +import { IFieldType } from '../../common'; +import { findIndexPatternById, getFieldByName } from '../index_patterns'; +import { shimAbortSignal } from '../search'; +import { getKbnServerError } from '../../../kibana_utils/server'; +import { ConfigSchema } from '../../config'; + +export async function termsEnumSuggestions( + config: ConfigSchema, + savedObjectsClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + index: string, + fieldName: string, + query: string, + filters?: estypes.QueryDslQueryContainer[], + field?: IFieldType, + abortSignal?: AbortSignal +) { + const { tiers } = config.autocomplete.valueSuggestions; + if (!field?.name && !field?.type) { + const indexPattern = await findIndexPatternById(savedObjectsClient, index); + field = indexPattern && getFieldByName(fieldName, indexPattern); + } + + try { + const promise = esClient.transport.request({ + method: 'POST', + path: encodeURI(`/${index}/_terms_enum`), + body: { + field: field?.name ?? field, + string: query, + index_filter: { + bool: { + must: [ + ...(filters ?? []), + { + terms: { + _tier: tiers, + }, + }, + ], + }, + }, + }, + }); + + const result = await shimAbortSignal(promise, abortSignal); + + return result.body.terms; + } catch (e) { + throw getKbnServerError(e); + } +} diff --git a/src/plugins/data/server/autocomplete/value_suggestions_route.ts b/src/plugins/data/server/autocomplete/value_suggestions_route.ts index 8fa14f8cbbd427..bd622d0151c93e 100644 --- a/src/plugins/data/server/autocomplete/value_suggestions_route.ts +++ b/src/plugins/data/server/autocomplete/value_suggestions_route.ts @@ -6,17 +6,15 @@ * Side Public License, v 1. */ -import { get, map } from 'lodash'; import { schema } from '@kbn/config-schema'; import { IRouter } from 'kibana/server'; - import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import type { estypes } from '@elastic/elasticsearch'; -import type { IFieldType } from '../index'; -import { findIndexPatternById, getFieldByName } from '../index_patterns'; import { getRequestAbortedSignal } from '../lib'; -import { ConfigSchema } from '../../config'; +import { getKbnServerError } from '../../../kibana_utils/server'; +import type { ConfigSchema } from '../../config'; +import { termsEnumSuggestions } from './terms_enum'; +import { termsAggSuggestions } from './terms_agg'; export function registerValueSuggestionsRoute(router: IRouter, config$: Observable) { router.post( @@ -44,88 +42,28 @@ export function registerValueSuggestionsRoute(router: IRouter, config$: Observab const config = await config$.pipe(first()).toPromise(); const { field: fieldName, query, filters, fieldMeta } = request.body; const { index } = request.params; - const { client } = context.core.elasticsearch.legacy; - const signal = getRequestAbortedSignal(request.events.aborted$); - - const autocompleteSearchOptions = { - timeout: `${config.autocomplete.valueSuggestions.timeout.asMilliseconds()}ms`, - terminate_after: config.autocomplete.valueSuggestions.terminateAfter.asMilliseconds(), - }; - - let field: IFieldType | undefined = fieldMeta; - - if (!field?.name && !field?.type) { - const indexPattern = await findIndexPatternById(context.core.savedObjects.client, index); - - field = indexPattern && getFieldByName(fieldName, indexPattern); + const abortSignal = getRequestAbortedSignal(request.events.aborted$); + + try { + const fn = + config.autocomplete.valueSuggestions.method === 'terms_enum' + ? termsEnumSuggestions + : termsAggSuggestions; + const body = await fn( + config, + context.core.savedObjects.client, + context.core.elasticsearch.client.asCurrentUser, + index, + fieldName, + query, + filters, + fieldMeta, + abortSignal + ); + return response.ok({ body }); + } catch (e) { + throw getKbnServerError(e); } - - const body = await getBody(autocompleteSearchOptions, field || fieldName, query, filters); - - const result = await client.callAsCurrentUser('search', { index, body }, { signal }); - - const buckets: any[] = - get(result, 'aggregations.suggestions.buckets') || - get(result, 'aggregations.nestedSuggestions.suggestions.buckets'); - - return response.ok({ body: map(buckets || [], 'key') }); } ); } - -async function getBody( - // eslint-disable-next-line @typescript-eslint/naming-convention - { timeout, terminate_after }: Record, - field: IFieldType | string, - query: string, - filters: estypes.QueryDslQueryContainer[] = [] -) { - const isFieldObject = (f: any): f is IFieldType => Boolean(f && f.name); - - // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators - const getEscapedQuery = (q: string = '') => - q.replace(/[.?+*|{}[\]()"\\#@&<>~]/g, (match) => `\\${match}`); - - // Helps ensure that the regex is not evaluated eagerly against the terms dictionary - const executionHint = 'map' as const; - - // We don't care about the accuracy of the counts, just the content of the terms, so this reduces - // the amount of information that needs to be transmitted to the coordinating node - const shardSize = 10; - const body = { - size: 0, - timeout, - terminate_after, - query: { - bool: { - filter: filters, - }, - }, - aggs: { - suggestions: { - terms: { - field: isFieldObject(field) ? field.name : field, - include: `${getEscapedQuery(query)}.*`, - execution_hint: executionHint, - shard_size: shardSize, - }, - }, - }, - }; - - if (isFieldObject(field) && field.subType && field.subType.nested) { - return { - ...body, - aggs: { - nestedSuggestions: { - nested: { - path: field.subType.nested.path, - }, - aggs: body.aggs, - }, - }, - }; - } - - return body; -} diff --git a/x-pack/test/functional/apps/maps/vector_styling.js b/x-pack/test/functional/apps/maps/vector_styling.js index f669416530c4e5..8b9e429242ca0e 100644 --- a/x-pack/test/functional/apps/maps/vector_styling.js +++ b/x-pack/test/functional/apps/maps/vector_styling.js @@ -34,7 +34,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.maps.setStyleByValue('fillColor', 'machine.os.raw'); await PageObjects.maps.selectCustomColorRamp('fillColor'); const suggestions = await PageObjects.maps.getCategorySuggestions(); - expect(suggestions.trim().split('\n').join()).to.equal('win 8,win xp,win 7,ios,osx'); + expect(suggestions.trim().split('\n').join()).to.equal('ios,osx,win 7,win 8,win xp'); }); }); }); From 7359fabf7c8350eaa40d8b753aec5236c8a2a34e Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 29 Jun 2021 18:17:51 -0500 Subject: [PATCH 008/121] [build] Remove OSS builds (#103685) --- .../development-package-tests.asciidoc | 2 +- src/dev/build/README.md | 4 +- src/dev/build/args.test.ts | 14 --- src/dev/build/args.ts | 5 - src/dev/build/build_distributables.ts | 4 - src/dev/build/cli.ts | 2 - src/dev/build/lib/build.test.ts | 39 ++----- src/dev/build/lib/build.ts | 12 +- src/dev/build/lib/runner.test.ts | 109 +----------------- src/dev/build/lib/runner.ts | 11 +- .../tasks/build_kibana_platform_plugins.ts | 1 - src/dev/build/tasks/build_packages_task.ts | 2 - src/dev/build/tasks/create_archives_task.ts | 4 +- src/dev/build/tasks/install_chromium.js | 24 ++-- src/dev/build/tasks/license_file_task.ts | 16 +-- .../os_packages/create_os_package_tasks.ts | 39 +++---- .../tasks/os_packages/docker_generator/run.ts | 12 +- src/dev/build/tasks/os_packages/run_fpm.ts | 12 +- test/scripts/jenkins_baseline.sh | 2 +- test/scripts/jenkins_build_kibana.sh | 2 +- test/scripts/jenkins_build_load_testing.sh | 2 +- test/scripts/jenkins_xpack_baseline.sh | 2 +- test/scripts/jenkins_xpack_package_build.sh | 2 +- 23 files changed, 67 insertions(+), 255 deletions(-) diff --git a/docs/developer/contributing/development-package-tests.asciidoc b/docs/developer/contributing/development-package-tests.asciidoc index 10c09d6cae8c07..7883ce2d832096 100644 --- a/docs/developer/contributing/development-package-tests.asciidoc +++ b/docs/developer/contributing/development-package-tests.asciidoc @@ -36,7 +36,7 @@ pip3 install --user ansible ``` # Build distributions -node scripts/build --all-platforms --debug --no-oss +node scripts/build --all-platforms --debug cd test/package diff --git a/src/dev/build/README.md b/src/dev/build/README.md index f6e11af67da33f..c499ef4a610832 100644 --- a/src/dev/build/README.md +++ b/src/dev/build/README.md @@ -12,8 +12,8 @@ node scripts/build --help # build a release version node scripts/build --release -# reuse already downloaded node executables, turn on debug logging, and only build the default distributable -node scripts/build --skip-node-download --debug --no-oss +# reuse already downloaded node executables, turn on debug logging +node scripts/build --skip-node-download --debug ``` # Fixing out of memory issues diff --git a/src/dev/build/args.test.ts b/src/dev/build/args.test.ts index e749af73241cf1..555df8981d70fb 100644 --- a/src/dev/build/args.test.ts +++ b/src/dev/build/args.test.ts @@ -26,8 +26,6 @@ it('build default and oss dist for current platform, without packages, by defaul expect(readCliArgs(['node', 'scripts/build'])).toMatchInlineSnapshot(` Object { "buildOptions": Object { - "buildDefaultDist": true, - "buildOssDist": true, "createArchives": true, "createDebPackage": false, "createDockerCentOS": false, @@ -53,8 +51,6 @@ it('builds packages if --all-platforms is passed', () => { expect(readCliArgs(['node', 'scripts/build', '--all-platforms'])).toMatchInlineSnapshot(` Object { "buildOptions": Object { - "buildDefaultDist": true, - "buildOssDist": true, "createArchives": true, "createDebPackage": true, "createDockerCentOS": true, @@ -80,8 +76,6 @@ it('limits packages if --rpm passed with --all-platforms', () => { expect(readCliArgs(['node', 'scripts/build', '--all-platforms', '--rpm'])).toMatchInlineSnapshot(` Object { "buildOptions": Object { - "buildDefaultDist": true, - "buildOssDist": true, "createArchives": true, "createDebPackage": false, "createDockerCentOS": false, @@ -107,8 +101,6 @@ it('limits packages if --deb passed with --all-platforms', () => { expect(readCliArgs(['node', 'scripts/build', '--all-platforms', '--deb'])).toMatchInlineSnapshot(` Object { "buildOptions": Object { - "buildDefaultDist": true, - "buildOssDist": true, "createArchives": true, "createDebPackage": true, "createDockerCentOS": false, @@ -135,8 +127,6 @@ it('limits packages if --docker passed with --all-platforms', () => { .toMatchInlineSnapshot(` Object { "buildOptions": Object { - "buildDefaultDist": true, - "buildOssDist": true, "createArchives": true, "createDebPackage": false, "createDockerCentOS": true, @@ -170,8 +160,6 @@ it('limits packages if --docker passed with --skip-docker-ubi and --all-platform ).toMatchInlineSnapshot(` Object { "buildOptions": Object { - "buildDefaultDist": true, - "buildOssDist": true, "createArchives": true, "createDebPackage": false, "createDockerCentOS": true, @@ -198,8 +186,6 @@ it('limits packages if --all-platforms passed with --skip-docker-centos', () => .toMatchInlineSnapshot(` Object { "buildOptions": Object { - "buildDefaultDist": true, - "buildOssDist": true, "createArchives": true, "createDebPackage": true, "createDockerCentOS": false, diff --git a/src/dev/build/args.ts b/src/dev/build/args.ts index bbfbd3e6f8813a..08a375e8011e69 100644 --- a/src/dev/build/args.ts +++ b/src/dev/build/args.ts @@ -15,8 +15,6 @@ export function readCliArgs(argv: string[]) { const unknownFlags: string[] = []; const flags = getopts(argv, { boolean: [ - 'oss', - 'no-oss', 'skip-archives', 'skip-initialize', 'skip-generic-folders', @@ -48,7 +46,6 @@ export function readCliArgs(argv: string[]) { rpm: null, deb: null, 'docker-images': null, - oss: null, 'version-qualifier': '', }, unknown: (flag) => { @@ -94,8 +91,6 @@ export function readCliArgs(argv: string[]) { const buildOptions: BuildOptions = { isRelease: Boolean(flags.release), versionQualifier: flags['version-qualifier'], - buildOssDist: flags.oss !== false, - buildDefaultDist: !flags.oss, initialize: !Boolean(flags['skip-initialize']), downloadFreshNode: !Boolean(flags['skip-node-download']), createGenericFolders: !Boolean(flags['skip-generic-folders']), diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index f0403fac1e26b3..159281ed71db0e 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -13,8 +13,6 @@ import * as Tasks from './tasks'; export interface BuildOptions { isRelease: boolean; - buildOssDist: boolean; - buildDefaultDist: boolean; downloadFreshNode: boolean; initialize: boolean; createGenericFolders: boolean; @@ -37,8 +35,6 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions const run = createRunner({ config, log, - buildDefaultDist: options.buildDefaultDist, - buildOssDist: options.buildOssDist, }); /** diff --git a/src/dev/build/cli.ts b/src/dev/build/cli.ts index 3712e2230296a1..c727c26d7dcd30 100644 --- a/src/dev/build/cli.ts +++ b/src/dev/build/cli.ts @@ -33,8 +33,6 @@ if (showHelp) { build the Kibana distributable options: - --oss {dim Only produce the OSS distributable of Kibana} - --no-oss {dim Only produce the default distributable of Kibana} --skip-archives {dim Don't produce tar/zip archives} --skip-os-packages {dim Don't produce rpm/deb/docker packages} --all-platforms {dim Produce archives for all platforms, not just this one} diff --git a/src/dev/build/lib/build.test.ts b/src/dev/build/lib/build.test.ts index fe328329d7df03..f92a91e2b7221e 100644 --- a/src/dev/build/lib/build.test.ts +++ b/src/dev/build/lib/build.test.ts @@ -43,40 +43,24 @@ beforeEach(() => { jest.clearAllMocks(); }); -const ossBuild = new Build(config, true); -const defaultBuild = new Build(config, false); - -describe('#isOss()', () => { - it('returns true for oss', () => { - expect(ossBuild.isOss()).toBe(true); - }); - - it('returns false for default build', () => { - expect(defaultBuild.isOss()).toBe(false); - }); -}); +const defaultBuild = new Build(config); describe('#getName()', () => { it('returns kibana for default build', () => { expect(defaultBuild.getName()).toBe('kibana'); }); - - it('returns kibana-oss for oss', () => { - expect(ossBuild.getName()).toBe('kibana-oss'); - }); }); describe('#getLogTag()', () => { it('returns string with build name in it', () => { expect(defaultBuild.getLogTag()).toContain(defaultBuild.getName()); - expect(ossBuild.getLogTag()).toContain(ossBuild.getName()); }); }); describe('#resolvePath()', () => { it('uses passed config to resolve a path relative to the repo', () => { - expect(ossBuild.resolvePath('bar')).toMatchInlineSnapshot( - `/build/kibana-oss/bar` + expect(defaultBuild.resolvePath('bar')).toMatchInlineSnapshot( + `/build/kibana/bar` ); }); @@ -89,28 +73,27 @@ describe('#resolvePath()', () => { describe('#resolvePathForPlatform()', () => { it('uses config.resolveFromRepo(), config.getBuildVersion(), and platform.getBuildName() to create path', () => { - expect(ossBuild.resolvePathForPlatform(linuxPlatform, 'foo', 'bar')).toMatchInlineSnapshot( - `/build/oss/kibana-8.0.0-linux-x86_64/foo/bar` + expect(defaultBuild.resolvePathForPlatform(linuxPlatform, 'foo', 'bar')).toMatchInlineSnapshot( + `/build/default/kibana-8.0.0-linux-x86_64/foo/bar` ); }); }); describe('#getPlatformArchivePath()', () => { it('creates correct path for different platforms', () => { - expect(ossBuild.getPlatformArchivePath(linuxPlatform)).toMatchInlineSnapshot( - `/target/kibana-oss-8.0.0-linux-x86_64.tar.gz` + expect(defaultBuild.getPlatformArchivePath(linuxPlatform)).toMatchInlineSnapshot( + `/target/kibana-8.0.0-linux-x86_64.tar.gz` ); - expect(ossBuild.getPlatformArchivePath(linuxArmPlatform)).toMatchInlineSnapshot( - `/target/kibana-oss-8.0.0-linux-aarch64.tar.gz` + expect(defaultBuild.getPlatformArchivePath(linuxArmPlatform)).toMatchInlineSnapshot( + `/target/kibana-8.0.0-linux-aarch64.tar.gz` ); - expect(ossBuild.getPlatformArchivePath(windowsPlatform)).toMatchInlineSnapshot( - `/target/kibana-oss-8.0.0-windows-x86_64.zip` + expect(defaultBuild.getPlatformArchivePath(windowsPlatform)).toMatchInlineSnapshot( + `/target/kibana-8.0.0-windows-x86_64.zip` ); }); describe('#getRootDirectory()', () => { it('creates correct root directory name', () => { - expect(ossBuild.getRootDirectory()).toMatchInlineSnapshot(`"kibana-oss-8.0.0"`); expect(defaultBuild.getRootDirectory()).toMatchInlineSnapshot(`"kibana-8.0.0"`); }); }); diff --git a/src/dev/build/lib/build.ts b/src/dev/build/lib/build.ts index f4ccb30994eef5..c777ad18dc51f0 100644 --- a/src/dev/build/lib/build.ts +++ b/src/dev/build/lib/build.ts @@ -12,14 +12,10 @@ import { Config } from './config'; import { Platform } from './platform'; export class Build { - private name = this.oss ? 'kibana-oss' : 'kibana'; - private logTag = this.oss ? chalk`{magenta [kibana-oss]}` : chalk`{cyan [ kibana ]}`; + private name = 'kibana'; + private logTag = chalk`{cyan [ kibana ]}`; - constructor(private config: Config, private oss: boolean) {} - - isOss() { - return !!this.oss; - } + constructor(private config: Config) {} resolvePath(...args: string[]) { return this.config.resolveFromRepo('build', this.name, ...args); @@ -28,7 +24,7 @@ export class Build { resolvePathForPlatform(platform: Platform, ...args: string[]) { return this.config.resolveFromRepo( 'build', - this.oss ? 'oss' : 'default', + 'default', `kibana-${this.config.getBuildVersion()}-${platform.getBuildName()}`, ...args ); diff --git a/src/dev/build/lib/runner.test.ts b/src/dev/build/lib/runner.test.ts index 0e3c00d220ad9a..2a08da2797a9de 100644 --- a/src/dev/build/lib/runner.test.ts +++ b/src/dev/build/lib/runner.test.ts @@ -45,7 +45,7 @@ beforeEach(() => { jest.clearAllMocks(); }); -const setup = async (opts: { buildDefaultDist: boolean; buildOssDist: boolean }) => { +const setup = async () => { const config = await Config.create({ isRelease: true, targetAllPlatforms: true, @@ -55,55 +55,14 @@ const setup = async (opts: { buildDefaultDist: boolean; buildOssDist: boolean }) const run = createRunner({ config, log, - ...opts, }); return { config, run }; }; -describe('buildOssDist = true, buildDefaultDist = true', () => { +describe('default dist', () => { it('runs global task once, passing config and log', async () => { - const { config, run } = await setup({ - buildDefaultDist: true, - buildOssDist: true, - }); - - const mock = jest.fn(); - - await run({ - global: true, - description: 'foo', - run: mock, - }); - - expect(mock).toHaveBeenCalledTimes(1); - expect(mock).toHaveBeenLastCalledWith(config, log, [expect.any(Build), expect.any(Build)]); - }); - - it('calls local tasks twice, passing each build', async () => { - const { config, run } = await setup({ - buildDefaultDist: true, - buildOssDist: true, - }); - - const mock = jest.fn(); - - await run({ - description: 'foo', - run: mock, - }); - - expect(mock).toHaveBeenCalledTimes(2); - expect(mock).toHaveBeenCalledWith(config, log, expect.any(Build)); - }); -}); - -describe('just default dist', () => { - it('runs global task once, passing config and log', async () => { - const { config, run } = await setup({ - buildDefaultDist: true, - buildOssDist: false, - }); + const { config, run } = await setup(); const mock = jest.fn(); @@ -118,52 +77,7 @@ describe('just default dist', () => { }); it('calls local tasks once, passing the default build', async () => { - const { config, run } = await setup({ - buildDefaultDist: true, - buildOssDist: false, - }); - - const mock = jest.fn(); - - await run({ - description: 'foo', - run: mock, - }); - - expect(mock).toHaveBeenCalledTimes(1); - expect(mock).toHaveBeenCalledWith(config, log, expect.any(Build)); - const [args] = mock.mock.calls; - const [, , build] = args; - if (build.isOss()) { - throw new Error('expected build to be the default dist, not the oss dist'); - } - }); -}); - -describe('just oss dist', () => { - it('runs global task once, passing config and log', async () => { - const { config, run } = await setup({ - buildDefaultDist: false, - buildOssDist: true, - }); - - const mock = jest.fn(); - - await run({ - global: true, - description: 'foo', - run: mock, - }); - - expect(mock).toHaveBeenCalledTimes(1); - expect(mock).toHaveBeenLastCalledWith(config, log, [expect.any(Build)]); - }); - - it('calls local tasks once, passing the oss build', async () => { - const { config, run } = await setup({ - buildDefaultDist: false, - buildOssDist: true, - }); + const { config, run } = await setup(); const mock = jest.fn(); @@ -174,20 +88,12 @@ describe('just oss dist', () => { expect(mock).toHaveBeenCalledTimes(1); expect(mock).toHaveBeenCalledWith(config, log, expect.any(Build)); - const [args] = mock.mock.calls; - const [, , build] = args; - if (!build.isOss()) { - throw new Error('expected build to be the oss dist, not the default dist'); - } }); }); describe('task rejection', () => { it('rejects, logs error, and marks error logged', async () => { - const { run } = await setup({ - buildDefaultDist: true, - buildOssDist: false, - }); + const { run } = await setup(); const error = new Error('FOO'); expect(isErrorLogged(error)).toBe(false); @@ -213,10 +119,7 @@ describe('task rejection', () => { }); it('just rethrows errors that have already been logged', async () => { - const { run } = await setup({ - buildDefaultDist: true, - buildOssDist: false, - }); + const { run } = await setup(); const error = markErrorLogged(new Error('FOO')); const promise = run({ diff --git a/src/dev/build/lib/runner.ts b/src/dev/build/lib/runner.ts index 015de6fe7e9ef7..1fccd884cc4f95 100644 --- a/src/dev/build/lib/runner.ts +++ b/src/dev/build/lib/runner.ts @@ -16,8 +16,6 @@ import { Config } from './config'; interface Options { config: Config; log: ToolingLog; - buildOssDist: boolean; - buildDefaultDist: boolean; } export interface GlobalTask { @@ -32,7 +30,7 @@ export interface Task { run(config: Config, log: ToolingLog, build: Build): Promise; } -export function createRunner({ config, log, buildOssDist, buildDefaultDist }: Options) { +export function createRunner({ config, log }: Options) { async function execTask(desc: string, task: Task | GlobalTask, lastArg: any) { log.info(desc); log.indent(4); @@ -63,12 +61,7 @@ export function createRunner({ config, log, buildOssDist, buildDefaultDist }: Op } const builds: Build[] = []; - if (buildDefaultDist) { - builds.push(new Build(config, false)); - } - if (buildOssDist) { - builds.push(new Build(config, true)); - } + builds.push(new Build(config)); /** * Run a task by calling its `run()` method with three arguments: diff --git a/src/dev/build/tasks/build_kibana_platform_plugins.ts b/src/dev/build/tasks/build_kibana_platform_plugins.ts index edff77d458f0f8..48d249ca374095 100644 --- a/src/dev/build/tasks/build_kibana_platform_plugins.ts +++ b/src/dev/build/tasks/build_kibana_platform_plugins.ts @@ -27,7 +27,6 @@ export const BuildKibanaPlatformPlugins: Task = { repoRoot: REPO_ROOT, outputRoot: build.resolvePath(), cache: false, - oss: build.isOss(), examples: false, watch: false, dist: true, diff --git a/src/dev/build/tasks/build_packages_task.ts b/src/dev/build/tasks/build_packages_task.ts index e6305b3761a4f7..808903661a5950 100644 --- a/src/dev/build/tasks/build_packages_task.ts +++ b/src/dev/build/tasks/build_packages_task.ts @@ -63,7 +63,6 @@ export const BuildBazelPackages: Task = { await buildBazelProductionProjects({ kibanaRoot: config.resolveFromRepo(), buildRoot: build.resolvePath(), - onlyOSS: build.isOss(), }); }, }; @@ -75,7 +74,6 @@ export const BuildPackages: Task = { await buildNonBazelProductionProjects({ kibanaRoot: config.resolveFromRepo(), buildRoot: build.resolvePath(), - onlyOSS: build.isOss(), }); }, }; diff --git a/src/dev/build/tasks/create_archives_task.ts b/src/dev/build/tasks/create_archives_task.ts index e2d3ff7149c4c5..37c4becae76a83 100644 --- a/src/dev/build/tasks/create_archives_task.ts +++ b/src/dev/build/tasks/create_archives_task.ts @@ -77,14 +77,14 @@ export const CreateArchives: Task = { const metrics: CiStatsMetric[] = []; for (const { format, path, fileCount } of archives) { metrics.push({ - group: `${build.isOss() ? 'oss ' : ''}distributable size`, + group: `distributable size`, id: format, value: (await asyncStat(path)).size, }); metrics.push({ group: 'distributable file count', - id: build.isOss() ? 'oss' : 'default', + id: 'default', value: fileCount, }); } diff --git a/src/dev/build/tasks/install_chromium.js b/src/dev/build/tasks/install_chromium.js index b21a8484fa7104..37abcbad4466eb 100644 --- a/src/dev/build/tasks/install_chromium.js +++ b/src/dev/build/tasks/install_chromium.js @@ -15,21 +15,17 @@ export const InstallChromium = { description: 'Installing Chromium', async run(config, log, build) { - if (build.isOss()) { - return; - } else { - for (const platform of config.getNodePlatforms()) { - log.info(`Installing Chromium for ${platform.getName()}-${platform.getArchitecture()}`); + for (const platform of config.getNodePlatforms()) { + 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(), - platform.getArchitecture() - ); - await binaryPath$.pipe(first()).toPromise(); - } + const { binaryPath$ } = installBrowser( + // TODO: https://github.com/elastic/kibana/issues/72496 + log, + build.resolvePathForPlatform(platform, 'x-pack/plugins/reporting/chromium'), + platform.getName(), + platform.getArchitecture() + ); + await binaryPath$.pipe(first()).toPromise(); } }, }; diff --git a/src/dev/build/tasks/license_file_task.ts b/src/dev/build/tasks/license_file_task.ts index 7e5ed8da0a27a9..ff33707476576f 100644 --- a/src/dev/build/tasks/license_file_task.ts +++ b/src/dev/build/tasks/license_file_task.ts @@ -8,23 +8,13 @@ import { write, read, Task } from '../lib'; -const LICENSE_SEPARATOR = `\n------------------------------------------------------------------------\n\n`; - export const UpdateLicenseFile: Task = { description: 'Updating LICENSE.txt file', async run(config, log, build) { const elasticLicense = await read(config.resolveFromRepo('licenses/ELASTIC-LICENSE-2.0.txt')); - if (build.isOss()) { - const ssplLicense = await read(config.resolveFromRepo('licenses/SSPL-LICENSE.txt')); - log.info('Copying dual-license to LICENSE.txt'); - await write( - build.resolvePath('LICENSE.txt'), - ssplLicense + LICENSE_SEPARATOR + elasticLicense - ); - } else { - log.info('Copying Elastic license to LICENSE.txt'); - await write(build.resolvePath('LICENSE.txt'), elasticLicense); - } + + log.info('Copying Elastic license to LICENSE.txt'); + await write(build.resolvePath('LICENSE.txt'), elasticLicense); }, }; diff --git a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts index 2ae882000cae00..99d0e1998e78a8 100644 --- a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts +++ b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts @@ -73,15 +73,12 @@ export const CreateDockerUBI: Task = { description: 'Creating Docker UBI image', async run(config, log, build) { - if (!build.isOss()) { - await runDockerGenerator(config, log, build, { - architecture: 'x64', - context: false, - ubi: true, - image: true, - dockerBuildDate, - }); - } + await runDockerGenerator(config, log, build, { + architecture: 'x64', + context: false, + ubi: true, + image: true, + }); }, }; @@ -95,19 +92,15 @@ export const CreateDockerContexts: Task = { dockerBuildDate, }); - if (!build.isOss()) { - await runDockerGenerator(config, log, build, { - ubi: true, - context: true, - image: false, - dockerBuildDate, - }); - await runDockerGenerator(config, log, build, { - ironbank: true, - context: true, - image: false, - dockerBuildDate, - }); - } + await runDockerGenerator(config, log, build, { + ubi: true, + context: true, + image: false, + }); + await runDockerGenerator(config, log, build, { + ironbank: true, + context: true, + image: false, + }); }, }; diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index c72112b7b6b03d..97fd7404097410 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -43,24 +43,18 @@ export async function runDockerGenerator( let imageFlavor = ''; if (flags.ubi) imageFlavor += `-${ubiVersionTag}`; if (flags.ironbank) imageFlavor += '-ironbank'; - if (build.isOss()) imageFlavor += '-oss'; // General docker var config - const license = build.isOss() ? 'ASL 2.0' : 'Elastic License'; + const license = 'Elastic License'; const imageTag = 'docker.elastic.co/kibana/kibana'; const version = config.getBuildVersion(); const artifactArchitecture = flags.architecture === 'aarch64' ? 'aarch64' : 'x86_64'; - const artifactFlavor = build.isOss() ? '-oss' : ''; - const artifactPrefix = `kibana${artifactFlavor}-${version}-linux`; + const artifactPrefix = `kibana-${version}-linux`; const artifactTarball = `${artifactPrefix}-${artifactArchitecture}.tar.gz`; const artifactsDir = config.resolveFromTarget('.'); const dockerBuildDate = flags.dockerBuildDate || new Date().toISOString(); // That would produce oss, default and default-ubi7 - const dockerBuildDir = config.resolveFromRepo( - 'build', - 'kibana-docker', - build.isOss() ? `oss` : `default${imageFlavor}` - ); + const dockerBuildDir = config.resolveFromRepo('build', 'kibana-docker', `default${imageFlavor}`); const imageArchitecture = flags.architecture === 'aarch64' ? '-aarch64' : ''; const dockerTargetFilename = config.resolveFromTarget( `kibana${imageFlavor}-${version}-docker-image${imageArchitecture}.tar.gz` diff --git a/src/dev/build/tasks/os_packages/run_fpm.ts b/src/dev/build/tasks/os_packages/run_fpm.ts index 933b3e411b2864..b732e4c80ea370 100644 --- a/src/dev/build/tasks/os_packages/run_fpm.ts +++ b/src/dev/build/tasks/os_packages/run_fpm.ts @@ -28,11 +28,7 @@ export async function runFpm( const fromBuild = (...paths: string[]) => build.resolvePathForPlatform(linux, ...paths); const pickLicense = () => { - if (build.isOss()) { - return type === 'rpm' ? 'ASL 2.0' : 'ASL-2.0'; - } else { - return type === 'rpm' ? 'Elastic License' : 'Elastic-License'; - } + return type === 'rpm' ? 'Elastic License' : 'Elastic-License'; }; const envFolder = type === 'rpm' ? 'sysconfig' : 'default'; @@ -57,7 +53,7 @@ export async function runFpm( // general info about the package '--name', - build.isOss() ? 'kibana-oss' : 'kibana', + 'kibana', '--description', 'Explore and visualize your Elasticsearch data', '--version', @@ -71,10 +67,6 @@ export async function runFpm( '--license', pickLicense(), - // prevent installing kibana if installing kibana-oss and vice versa - '--conflicts', - build.isOss() ? 'kibana' : 'kibana-oss', - // define install/uninstall scripts '--after-install', resolve(__dirname, 'package_scripts/post_install.sh'), diff --git a/test/scripts/jenkins_baseline.sh b/test/scripts/jenkins_baseline.sh index 40bfc6e83ad1bf..5c0ca5a8e01ca1 100755 --- a/test/scripts/jenkins_baseline.sh +++ b/test/scripts/jenkins_baseline.sh @@ -4,7 +4,7 @@ source src/dev/ci_setup/setup_env.sh source "$KIBANA_DIR/src/dev/ci_setup/setup_percy.sh" echo " -> building and extracting OSS Kibana distributable for use in functional tests" -node scripts/build --debug --oss +node scripts/build --debug echo " -> shipping metrics from build to ci-stats" node scripts/ship_ci_stats \ diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index 198723908cf487..a9edd3ed2a701b 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -33,7 +33,7 @@ node x-pack/scripts/functional_tests --assert-none-excluded \ # Do not build kibana for code coverage run if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> building and extracting default Kibana distributable for use in functional tests" - node scripts/build --debug --no-oss + node scripts/build --debug echo " -> shipping metrics from build to ci-stats" node scripts/ship_ci_stats \ diff --git a/test/scripts/jenkins_build_load_testing.sh b/test/scripts/jenkins_build_load_testing.sh index 5571eee4f28edb..d7c7bda83c9ef2 100755 --- a/test/scripts/jenkins_build_load_testing.sh +++ b/test/scripts/jenkins_build_load_testing.sh @@ -60,7 +60,7 @@ export KBN_NP_PLUGINS_BUILT=true echo " -> Building and extracting default Kibana distributable for use in functional tests" cd "$KIBANA_DIR" -node scripts/build --debug --no-oss +node scripts/build --debug linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$KIBANA_DIR/install/kibana" mkdir -p "$installDir" diff --git a/test/scripts/jenkins_xpack_baseline.sh b/test/scripts/jenkins_xpack_baseline.sh index 8d5624949505a5..c68c0f40902f22 100755 --- a/test/scripts/jenkins_xpack_baseline.sh +++ b/test/scripts/jenkins_xpack_baseline.sh @@ -5,7 +5,7 @@ source "$KIBANA_DIR/src/dev/ci_setup/setup_percy.sh" echo " -> building and extracting default Kibana distributable" cd "$KIBANA_DIR" -node scripts/build --debug --no-oss +node scripts/build --debug echo " -> shipping metrics from build to ci-stats" node scripts/ship_ci_stats \ diff --git a/test/scripts/jenkins_xpack_package_build.sh b/test/scripts/jenkins_xpack_package_build.sh index 698129a2d253bd..86e846f720803f 100755 --- a/test/scripts/jenkins_xpack_package_build.sh +++ b/test/scripts/jenkins_xpack_package_build.sh @@ -7,6 +7,6 @@ source src/dev/ci_setup/setup_env.sh export TMP=/tmp export TMPDIR=/tmp -node scripts/build --all-platforms --debug --no-oss +node scripts/build --all-platforms --debug gsutil -q -m cp 'target/*' "gs://ci-artifacts.kibana.dev/package-testing/$GIT_COMMIT/" From 61d23665a9e391c3cfa42021671ef3461f0c9560 Mon Sep 17 00:00:00 2001 From: Elizabet Oliveira Date: Wed, 30 Jun 2021 00:32:55 +0100 Subject: [PATCH 009/121] [Maps] Add layer disabled button styles (#103775) --- .../right_side_controls/layer_control/_layer_control.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/_layer_control.scss b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/_layer_control.scss index 19f70070ef5b2b..6e31d3f26b4bc4 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/_layer_control.scss +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/_layer_control.scss @@ -9,6 +9,12 @@ .mapLayerControl__addLayerButton { flex-shrink: 0; + + // overrides disabled color that is transparent and doesn't work well on top of the map + &.euiButton-isDisabled { + // sass-lint:disable-block no-important + background-color: $euiColorLightShade !important; + } } .mapLayerControl__addLayerButton, From f039f8311f342c0b9510fe49f79369f417007c77 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Tue, 29 Jun 2021 18:41:17 -0500 Subject: [PATCH 010/121] Deprecate APM OSS `fleetMode` setting (#103721) This has been enabled by default, and there isn't any remaining reason to turn it off. Mark it as deprecated and remove conditionals that check for it. --- src/plugins/apm_oss/server/index.ts | 5 ++- x-pack/plugins/apm/server/index.test.ts | 46 +++---------------------- x-pack/plugins/apm/server/index.ts | 27 +++++++-------- 3 files changed, 21 insertions(+), 57 deletions(-) diff --git a/src/plugins/apm_oss/server/index.ts b/src/plugins/apm_oss/server/index.ts index 1424cb1c7126fb..7b16c42f4c9b91 100644 --- a/src/plugins/apm_oss/server/index.ts +++ b/src/plugins/apm_oss/server/index.ts @@ -7,9 +7,11 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from '../../../core/server'; +import { ConfigDeprecationProvider, PluginInitializerContext } from '../../../core/server'; import { APMOSSPlugin } from './plugin'; +const deprecations: ConfigDeprecationProvider = ({ unused }) => [unused('fleetMode')]; + export const config = { schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), @@ -22,6 +24,7 @@ export const config = { indexPattern: schema.string({ defaultValue: 'apm-*' }), fleetMode: schema.boolean({ defaultValue: true }), }), + deprecations, }; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/x-pack/plugins/apm/server/index.test.ts b/x-pack/plugins/apm/server/index.test.ts index 226dfd6e95bd31..6052ec921f9f9c 100644 --- a/x-pack/plugins/apm/server/index.test.ts +++ b/x-pack/plugins/apm/server/index.test.ts @@ -28,53 +28,15 @@ describe('mergeConfigs', () => { agent: { migrations: { enabled: true } }, } as APMXPackConfig; - expect(mergeConfigs(apmOssConfig, apmConfig)).toEqual({ - 'apm_oss.errorIndices': 'apm-*-error-*', - 'apm_oss.indexPattern': 'apm-*', - 'apm_oss.metricsIndices': 'apm-*-metric-*', - 'apm_oss.spanIndices': 'apm-*-span-*', - 'apm_oss.transactionIndices': 'apm-*-transaction-*', - 'xpack.apm.metricsInterval': 2000, - 'xpack.apm.ui.enabled': false, - 'xpack.apm.agent.migrations.enabled': true, - }); - }); - - it('adds fleet indices', () => { - const apmOssConfig = { - transactionIndices: 'apm-*-transaction-*', - spanIndices: 'apm-*-span-*', - errorIndices: 'apm-*-error-*', - metricsIndices: 'apm-*-metric-*', - fleetMode: true, - } as APMOSSConfig; - - const apmConfig = { ui: {}, agent: { migrations: {} } } as APMXPackConfig; - expect(mergeConfigs(apmOssConfig, apmConfig)).toEqual({ 'apm_oss.errorIndices': 'logs-apm*,apm-*-error-*', + 'apm_oss.indexPattern': 'apm-*', 'apm_oss.metricsIndices': 'metrics-apm*,apm-*-metric-*', 'apm_oss.spanIndices': 'traces-apm*,apm-*-span-*', 'apm_oss.transactionIndices': 'traces-apm*,apm-*-transaction-*', - }); - }); - - it('does not add fleet indices', () => { - const apmOssConfig = { - transactionIndices: 'apm-*-transaction-*', - spanIndices: 'apm-*-span-*', - errorIndices: 'apm-*-error-*', - metricsIndices: 'apm-*-metric-*', - fleetMode: false, - } as APMOSSConfig; - - const apmConfig = { ui: {}, agent: { migrations: {} } } as APMXPackConfig; - - expect(mergeConfigs(apmOssConfig, apmConfig)).toEqual({ - 'apm_oss.errorIndices': 'apm-*-error-*', - 'apm_oss.metricsIndices': 'apm-*-metric-*', - 'apm_oss.spanIndices': 'apm-*-span-*', - 'apm_oss.transactionIndices': 'apm-*-transaction-*', + 'xpack.apm.metricsInterval': 2000, + 'xpack.apm.ui.enabled': false, + 'xpack.apm.agent.migrations.enabled': true, }); }); }); diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 413efcdb788128..8ec92bfa7a1b58 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -102,23 +102,22 @@ export function mergeConfigs( 'xpack.apm.agent.migrations.enabled': apmConfig.agent.migrations.enabled, }; - if (apmOssConfig.fleetMode) { - mergedConfig[ - 'apm_oss.transactionIndices' - ] = `traces-apm*,${mergedConfig['apm_oss.transactionIndices']}`; + // Add data stream indices to list of configured values + mergedConfig[ + 'apm_oss.transactionIndices' + ] = `traces-apm*,${mergedConfig['apm_oss.transactionIndices']}`; - mergedConfig[ - 'apm_oss.spanIndices' - ] = `traces-apm*,${mergedConfig['apm_oss.spanIndices']}`; + mergedConfig[ + 'apm_oss.spanIndices' + ] = `traces-apm*,${mergedConfig['apm_oss.spanIndices']}`; - mergedConfig[ - 'apm_oss.errorIndices' - ] = `logs-apm*,${mergedConfig['apm_oss.errorIndices']}`; + mergedConfig[ + 'apm_oss.errorIndices' + ] = `logs-apm*,${mergedConfig['apm_oss.errorIndices']}`; - mergedConfig[ - 'apm_oss.metricsIndices' - ] = `metrics-apm*,${mergedConfig['apm_oss.metricsIndices']}`; - } + mergedConfig[ + 'apm_oss.metricsIndices' + ] = `metrics-apm*,${mergedConfig['apm_oss.metricsIndices']}`; return mergedConfig; } From e749fa62fa2e9ed2799630768b82b4492fc57827 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Tue, 29 Jun 2021 19:42:38 -0400 Subject: [PATCH 011/121] [Security Solution][Endpoint][Host Isolation][Cases] Update Host Isolation comment in Cases UI (#102937) --- .../plugins/cases/common/api/cases/comment.ts | 43 +- .../components/case_view/index.test.tsx | 20 + .../public/components/case_view/index.tsx | 5 + .../components/user_action_tree/helpers.tsx | 78 ++- .../user_action_tree/index.test.tsx | 1 + .../components/user_action_tree/index.tsx | 30 ++ .../user_action_tree/translations.ts | 14 + ...er_action_host_isolation_comment_event.tsx | 56 ++ x-pack/plugins/cases/server/client/utils.ts | 4 + x-pack/plugins/cases/server/common/utils.ts | 9 + .../server/saved_object_types/comments.ts | 12 + .../common/endpoint/schema/actions.ts | 4 +- .../cases/components/case_view/index.tsx | 23 + .../common/lib/endpoint_isolation/mocks.ts | 1 - .../public/management/common/routing.ts | 10 +- .../endpoint/routes/actions/isolation.test.ts | 507 +++++++++--------- .../endpoint/routes/actions/isolation.ts | 51 +- .../server/endpoint/services/index.ts | 2 +- .../services/{lookup_agent.ts => metadata.ts} | 7 +- 19 files changed, 591 insertions(+), 286 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.tsx rename x-pack/plugins/security_solution/server/endpoint/services/{lookup_agent.ts => metadata.ts} (89%) diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts index 746c28f9942392..378a89f8af8c3c 100644 --- a/x-pack/plugins/cases/common/api/cases/comment.ts +++ b/x-pack/plugins/cases/common/api/cases/comment.ts @@ -39,6 +39,7 @@ export enum CommentType { user = 'user', alert = 'alert', generatedAlert = 'generated_alert', + actions = 'actions', } export const ContextTypeUserRt = rt.type({ @@ -63,11 +64,38 @@ export const AlertCommentRequestRt = rt.type({ owner: rt.string, }); +export const ActionsCommentRequestRt = rt.type({ + type: rt.literal(CommentType.actions), + comment: rt.string, + actions: rt.type({ + targets: rt.array( + rt.type({ + hostname: rt.string, + endpointId: rt.string, + }) + ), + type: rt.string, + }), + owner: rt.string, +}); + const AttributesTypeUserRt = rt.intersection([ContextTypeUserRt, CommentAttributesBasicRt]); const AttributesTypeAlertsRt = rt.intersection([AlertCommentRequestRt, CommentAttributesBasicRt]); -const CommentAttributesRt = rt.union([AttributesTypeUserRt, AttributesTypeAlertsRt]); +const AttributesTypeActionsRt = rt.intersection([ + ActionsCommentRequestRt, + CommentAttributesBasicRt, +]); +const CommentAttributesRt = rt.union([ + AttributesTypeUserRt, + AttributesTypeAlertsRt, + AttributesTypeActionsRt, +]); -export const CommentRequestRt = rt.union([ContextTypeUserRt, AlertCommentRequestRt]); +export const CommentRequestRt = rt.union([ + ContextTypeUserRt, + AlertCommentRequestRt, + ActionsCommentRequestRt, +]); export const CommentResponseRt = rt.intersection([ CommentAttributesRt, @@ -85,6 +113,14 @@ export const CommentResponseTypeAlertsRt = rt.intersection([ }), ]); +export const CommentResponseTypeActionsRt = rt.intersection([ + AttributesTypeActionsRt, + rt.type({ + id: rt.string, + version: rt.string, + }), +]); + export const AllCommentsResponseRT = rt.array(CommentResponseRt); export const CommentPatchRequestRt = rt.intersection([ @@ -125,15 +161,18 @@ export const FindQueryParamsRt = rt.partial({ }); export type FindQueryParams = rt.TypeOf; +export type AttributesTypeActions = rt.TypeOf; export type AttributesTypeAlerts = rt.TypeOf; export type AttributesTypeUser = rt.TypeOf; export type CommentAttributes = rt.TypeOf; export type CommentRequest = rt.TypeOf; export type CommentResponse = rt.TypeOf; export type CommentResponseAlertsType = rt.TypeOf; +export type CommentResponseActionsType = rt.TypeOf; export type AllCommentsResponse = rt.TypeOf; export type CommentsResponse = rt.TypeOf; export type CommentPatchRequest = rt.TypeOf; export type CommentPatchAttributes = rt.TypeOf; export type CommentRequestUserType = rt.TypeOf; export type CommentRequestAlertType = rt.TypeOf; +export type CommentRequestActionsType = rt.TypeOf; diff --git a/x-pack/plugins/cases/public/components/case_view/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/index.test.tsx index 1fafbac50c2b9a..e04bbbe54c8374 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.test.tsx @@ -90,6 +90,10 @@ export const caseProps: CaseComponentProps = { }, getCaseDetailHrefWithCommentId: jest.fn(), onComponentInitialized: jest.fn(), + actionsNavigation: { + href: jest.fn(), + onClick: jest.fn(), + }, ruleDetailsNavigation: { href: jest.fn(), onClick: jest.fn(), @@ -408,6 +412,10 @@ describe('CaseView ', () => { }, getCaseDetailHrefWithCommentId: jest.fn(), onComponentInitialized: jest.fn(), + actionsNavigation: { + href: jest.fn(), + onClick: jest.fn(), + }, ruleDetailsNavigation: { href: jest.fn(), onClick: jest.fn(), @@ -448,6 +456,10 @@ describe('CaseView ', () => { }, getCaseDetailHrefWithCommentId: jest.fn(), onComponentInitialized: jest.fn(), + actionsNavigation: { + href: jest.fn(), + onClick: jest.fn(), + }, ruleDetailsNavigation: { href: jest.fn(), onClick: jest.fn(), @@ -485,6 +497,10 @@ describe('CaseView ', () => { }, getCaseDetailHrefWithCommentId: jest.fn(), onComponentInitialized: jest.fn(), + actionsNavigation: { + href: jest.fn(), + onClick: jest.fn(), + }, ruleDetailsNavigation: { href: jest.fn(), onClick: jest.fn(), @@ -522,6 +538,10 @@ describe('CaseView ', () => { }, getCaseDetailHrefWithCommentId: jest.fn(), onComponentInitialized: jest.fn(), + actionsNavigation: { + href: jest.fn(), + onClick: jest.fn(), + }, ruleDetailsNavigation: { href: jest.fn(), onClick: jest.fn(), diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index 2fffd6464dbb1e..ac7c9ebe08b5ab 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -50,6 +50,7 @@ export interface CaseViewComponentProps { configureCasesNavigation: CasesNavigation; getCaseDetailHrefWithCommentId: (commentId: string) => string; onComponentInitialized?: () => void; + actionsNavigation?: CasesNavigation; ruleDetailsNavigation?: CasesNavigation; showAlertDetails?: (alertId: string, index: string) => void; subCaseId?: string; @@ -99,6 +100,7 @@ export const CaseComponent = React.memo( getCaseDetailHrefWithCommentId, fetchCase, onComponentInitialized, + actionsNavigation, ruleDetailsNavigation, showAlertDetails, subCaseId, @@ -418,6 +420,7 @@ export const CaseComponent = React.memo( caseUserActions={caseUserActions} connectors={connectors} data={caseData} + actionsNavigation={actionsNavigation} fetchUserActions={fetchCaseUserActions.bind( null, caseId, @@ -505,6 +508,7 @@ export const CaseView = React.memo( getCaseDetailHrefWithCommentId, onCaseDataSuccess, onComponentInitialized, + actionsNavigation, ruleDetailsNavigation, showAlertDetails, subCaseId, @@ -543,6 +547,7 @@ export const CaseView = React.memo( getCaseDetailHrefWithCommentId={getCaseDetailHrefWithCommentId} fetchCase={fetchCase} onComponentInitialized={onComponentInitialized} + actionsNavigation={actionsNavigation} ruleDetailsNavigation={ruleDetailsNavigation} showAlertDetails={showAlertDetails} subCaseId={subCaseId} diff --git a/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx b/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx index 5424ad8238a2ac..5d234296dd5033 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx @@ -7,12 +7,14 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiCommentProps } from '@elastic/eui'; import React from 'react'; - +import classNames from 'classnames'; import { CaseFullExternalService, ActionConnector, CaseStatuses, CommentType, + Comment, + CommentRequestActionsType, } from '../../../common'; import { CaseUserActions } from '../../containers/types'; import { CaseServices } from '../../containers/use_get_case_user_actions'; @@ -20,13 +22,16 @@ import { parseString } from '../../containers/utils'; import { Tags } from '../tag_list/tags'; import { UserActionUsernameWithAvatar } from './user_action_username_with_avatar'; import { UserActionTimestamp } from './user_action_timestamp'; +import { UserActionContentToolbar } from './user_action_content_toolbar'; import { UserActionCopyLink } from './user_action_copy_link'; +import { UserActionMarkdown } from './user_action_markdown'; import { UserActionMoveToReference } from './user_action_move_to_reference'; import { Status, statuses } from '../status'; import { UserActionShowAlert } from './user_action_show_alert'; import * as i18n from './translations'; import { AlertCommentEvent } from './user_action_alert_comment_event'; import { CasesNavigation } from '../links'; +import { HostIsolationCommentEvent } from './user_action_host_isolation_comment_event'; interface LabelTitle { action: CaseUserActions; @@ -34,6 +39,8 @@ interface LabelTitle { } export type RuleDetailsNavigation = CasesNavigation; +export type ActionsNavigation = CasesNavigation; + const getStatusTitle = (id: string, status: CaseStatuses) => ( string; + actionsNavigation?: ActionsNavigation; + manageMarkdownEditIds: string[]; + handleManageMarkdownEditId: (id: string) => void; + handleManageQuote: (id: string) => void; + handleSaveComment: ({ id, version }: { id: string; version: string }, content: string) => void; + action: CaseUserActions; +}): EuiCommentProps => ({ + username: ( + + ), + className: classNames({ + isEdit: manageMarkdownEditIds.includes(comment.id), + }), + event: ( + + ), + 'data-test-subj': 'endpoint-action', + timestamp: , + timelineIcon: comment.actions.type === 'isolate' ? 'lock' : 'lockOpen', + actions: ( + + ), + children: ( + + ), +}); + interface Signal { rule: { id: string; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx index faa4f1d1a786fc..610399c31928b7 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx @@ -28,6 +28,7 @@ const defaultProps = { caseUserActions: [], connectors: [], getCaseDetailHrefWithCommentId: jest.fn(), + actionsNavigation: { href: jest.fn(), onClick: jest.fn() }, getRuleDetailsHref: jest.fn(), onRuleDetailsClick: jest.fn(), data: basicCase, diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx index 5e90c2e6a951fb..eb74eb2bd643a6 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx @@ -26,6 +26,7 @@ import { useCurrentUser } from '../../common/lib/kibana'; import { AddComment, AddCommentRefObject } from '../add_comment'; import { ActionConnector, + ActionsCommentRequestRt, AlertCommentRequestRt, Case, CaseUserActions, @@ -45,6 +46,8 @@ import { getAlertAttachment, getGeneratedAlertsAttachment, RuleDetailsNavigation, + ActionsNavigation, + getActionAttachment, } from './helpers'; import { UserActionAvatar } from './user_action_avatar'; import { UserActionMarkdown } from './user_action_markdown'; @@ -61,6 +64,7 @@ export interface UserActionTreeProps { fetchUserActions: () => void; getCaseDetailHrefWithCommentId: (commentId: string) => string; getRuleDetailsHref?: RuleDetailsNavigation['href']; + actionsNavigation?: ActionsNavigation; isLoadingDescription: boolean; isLoadingUserActions: boolean; onRuleDetailsClick?: RuleDetailsNavigation['onClick']; @@ -125,6 +129,7 @@ export const UserActionTree = React.memo( fetchUserActions, getCaseDetailHrefWithCommentId, getRuleDetailsHref, + actionsNavigation, isLoadingDescription, isLoadingUserActions, onRuleDetailsClick, @@ -447,6 +452,30 @@ export const UserActionTree = React.memo( ] : []), ]; + } else if ( + comment != null && + isRight(ActionsCommentRequestRt.decode(comment)) && + comment.type === CommentType.actions + ) { + return [ + ...comments, + ...(comment.actions !== null + ? [ + getActionAttachment({ + comment, + userCanCrud, + isLoadingIds, + getCaseDetailHrefWithCommentId, + actionsNavigation, + manageMarkdownEditIds, + handleManageMarkdownEditId, + handleManageQuote, + handleSaveComment, + action, + }), + ] + : []), + ]; } } @@ -559,6 +588,7 @@ export const UserActionTree = React.memo( handleManageMarkdownEditId, handleSaveComment, getCaseDetailHrefWithCommentId, + actionsNavigation, userCanCrud, isLoadingIds, handleManageQuote, diff --git a/x-pack/plugins/cases/public/components/user_action_tree/translations.ts b/x-pack/plugins/cases/public/components/user_action_tree/translations.ts index 27d1554ed255b6..54738e29060f3d 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/translations.ts +++ b/x-pack/plugins/cases/public/components/user_action_tree/translations.ts @@ -56,3 +56,17 @@ export const SHOW_ALERT_TOOLTIP = i18n.translate('xpack.cases.caseView.showAlert export const UNKNOWN_RULE = i18n.translate('xpack.cases.caseView.unknownRule.label', { defaultMessage: 'Unknown rule', }); + +export const ISOLATED_HOST = i18n.translate('xpack.cases.caseView.isolatedHost', { + defaultMessage: 'isolated host', +}); + +export const RELEASED_HOST = i18n.translate('xpack.cases.caseView.releasedHost', { + defaultMessage: 'released host', +}); + +export const OTHER_ENDPOINTS = (endpoints: number): string => + i18n.translate('xpack.cases.caseView.otherEndpoints', { + values: { endpoints }, + defaultMessage: ` and {endpoints} {endpoints, plural, =1 {other} other {others}}`, + }); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.tsx new file mode 100644 index 00000000000000..d363e874a4e0dd --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.tsx @@ -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. + */ + +import React, { memo, useCallback } from 'react'; +import * as i18n from './translations'; +import { LinkAnchor } from '../links'; +import { ActionsNavigation } from './helpers'; + +interface EndpointInfo { + endpointId: string; + hostname: string; +} + +interface Props { + type: string; + endpoints: EndpointInfo[]; + href?: ActionsNavigation['href']; + onClick?: ActionsNavigation['onClick']; +} + +const HostIsolationCommentEventComponent: React.FC = ({ + type, + endpoints, + href, + onClick, +}) => { + const endpointDetailsHref = href ? href(endpoints[0].endpointId) : ''; + + const onLinkClick = useCallback( + (ev) => { + ev.preventDefault(); + if (onClick) onClick(endpoints[0].endpointId, ev); + }, + [onClick, endpoints] + ); + + return ( + <> + {type === 'isolate' ? `${i18n.ISOLATED_HOST} ` : `${i18n.RELEASED_HOST} `} + + {endpoints[0].hostname} + + {endpoints.length > 1 && i18n.OTHER_ENDPOINTS(endpoints.length - 1)} + + ); +}; + +export const HostIsolationCommentEvent = memo(HostIsolationCommentEventComponent); diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index 3240ec06803c77..0e7a21816de4c1 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -17,6 +17,7 @@ import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common'; import { esKuery } from '../../../../../src/plugins/data/server'; import { AlertCommentRequestRt, + ActionsCommentRequestRt, CASE_SAVED_OBJECT, CaseConnector, CaseStatuses, @@ -35,12 +36,15 @@ import { getIDsAndIndicesAsArrays, isCommentRequestTypeAlertOrGenAlert, isCommentRequestTypeUser, + isCommentRequestTypeActions, SavedObjectFindOptionsKueryNode, } from '../common'; export const decodeCommentRequest = (comment: CommentRequest) => { if (isCommentRequestTypeUser(comment)) { pipe(excess(ContextTypeUserRt).decode(comment), fold(throwErrors(badRequest), identity)); + } else if (isCommentRequestTypeActions(comment)) { + pipe(excess(ActionsCommentRequestRt).decode(comment), fold(throwErrors(badRequest), identity)); } else if (isCommentRequestTypeAlertOrGenAlert(comment)) { pipe(excess(AlertCommentRequestRt).decode(comment), fold(throwErrors(badRequest), identity)); const { ids, indices } = getIDsAndIndicesAsArrays(comment); diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index 70ecc50df0f488..13d3f3768f3919 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -317,6 +317,15 @@ export const isCommentRequestTypeUser = ( return context.type === CommentType.user; }; +/** + * A type narrowing function for actions comments. Exporting so integration tests can use it. + */ +export const isCommentRequestTypeActions = ( + context: CommentRequest +): context is CommentRequestUserType => { + return context.type === CommentType.actions; +}; + /** * A type narrowing function for alert comments. Exporting so integration tests can use it. */ diff --git a/x-pack/plugins/cases/server/saved_object_types/comments.ts b/x-pack/plugins/cases/server/saved_object_types/comments.ts index cd2dec7260fa4a..876ceb9bc2045a 100644 --- a/x-pack/plugins/cases/server/saved_object_types/comments.ts +++ b/x-pack/plugins/cases/server/saved_object_types/comments.ts @@ -27,6 +27,18 @@ export const caseCommentSavedObjectType: SavedObjectsType = { type: { type: 'keyword', }, + actions: { + properties: { + targets: { + type: 'nested', + properties: { + hostname: { type: 'keyword' }, + endpointId: { type: 'keyword' }, + }, + }, + type: { type: 'keyword' }, + }, + }, alertId: { type: 'keyword', }, 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 f58dd1f3370d4a..fd4d89540f0ce9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts @@ -9,10 +9,8 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const HostIsolationRequestSchema = { body: schema.object({ - /** A list of Fleet Agent IDs whose hosts will be isolated */ - agent_ids: schema.maybe(schema.arrayOf(schema.string())), /** A list of endpoint IDs whose hosts will be isolated (Fleet Agent IDs will be retrieved for these) */ - endpoint_ids: schema.maybe(schema.arrayOf(schema.string())), + endpoint_ids: schema.arrayOf(schema.string(), { minSize: 1 }), /** If defined, any case associated with the given IDs will be updated */ alert_ids: schema.maybe(schema.arrayOf(schema.string())), /** Case IDs to be updated */ diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 5474fcb47d87e1..10a9e27fee1cfc 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -35,6 +35,7 @@ import { useInsertTimeline } from '../use_insert_timeline'; import { SpyRoute } from '../../../common/utils/route/spy_routes'; import * as timelineMarkdownPlugin from '../../../common/components/markdown_editor/plugins/timeline'; import { CaseDetailsRefreshContext } from '../../../common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context'; +import { getEndpointDetailsPath } from '../../../management/common/routing'; interface Props { caseId: string; @@ -162,6 +163,14 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = [dispatch] ); + const endpointDetailsHref = (endpointId: string) => + formatUrl( + getEndpointDetailsPath({ + name: 'endpointActivityLog', + selected_endpoint: endpointId, + }) + ); + const onComponentInitialized = useCallback(() => { dispatch( timelineActions.createTimeline({ @@ -220,6 +229,20 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = getCaseDetailHrefWithCommentId, onCaseDataSuccess, onComponentInitialized, + actionsNavigation: { + href: endpointDetailsHref, + onClick: (endpointId: string, e) => { + if (e) { + e.preventDefault(); + } + return navigateToApp(APP_ID, { + path: getEndpointDetailsPath({ + name: 'endpointActivityLog', + selected_endpoint: endpointId, + }), + }); + }, + }, ruleDetailsNavigation: { href: getDetectionsRuleDetailsHref, onClick: async (ruleId: string | null | undefined, e) => { diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint_isolation/mocks.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint_isolation/mocks.ts index 61989184963163..256359e40b6e27 100644 --- a/x-pack/plugins/security_solution/public/common/lib/endpoint_isolation/mocks.ts +++ b/x-pack/plugins/security_solution/public/common/lib/endpoint_isolation/mocks.ts @@ -14,7 +14,6 @@ import { ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE } from '../../../../common/end export const hostIsolationRequestBodyMock = (): HostIsolationRequestBody => { return { - agent_ids: ['fd8a122b-4c54-4c05-b295-111'], endpoint_ids: ['88c04a90-b19c-11eb-b838-222'], alert_ids: ['88c04a90-b19c-11eb-b838-333'], case_ids: ['88c04a90-b19c-11eb-b838-444'], diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index 93d0642c6b3b6f..9cb25dd4bb5a57 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -66,7 +66,12 @@ export const getEndpointListPath = ( export const getEndpointDetailsPath = ( props: { - name: 'endpointDetails' | 'endpointPolicyResponse' | 'endpointIsolate' | 'endpointUnIsolate'; + name: + | 'endpointDetails' + | 'endpointPolicyResponse' + | 'endpointIsolate' + | 'endpointUnIsolate' + | 'endpointActivityLog'; } & EndpointIndexUIQueryParams & EndpointDetailsUrlProps, search?: string @@ -85,6 +90,9 @@ export const getEndpointDetailsPath = ( case 'endpointPolicyResponse': queryParams.show = 'policy_response'; break; + case 'endpointActivityLog': + queryParams.show = 'activity_log'; + break; } const urlQueryParams = querystringStringify( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts index 5ce27164dc8787..1b1490d3af0727 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts @@ -22,6 +22,7 @@ import { createMockPackageService, createRouteHandlerContext, } from '../../mocks'; +import { HostIsolationRequestSchema } from '../../../../common/endpoint/schema/actions'; import { registerHostIsolationRoutes } from './isolation'; import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { LicenseService } from '../../../../common/license'; @@ -55,282 +56,310 @@ const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold' } }); describe('Host Isolation', () => { - let endpointAppContextService: EndpointAppContextService; - let mockResponse: jest.Mocked; - let licenseService: LicenseService; - let licenseEmitter: Subject; + describe('schema', () => { + it('should require at least 1 Endpoint ID', () => { + expect(() => { + HostIsolationRequestSchema.body.validate({}); + }).toThrow(); + }); - let callRoute: ( - routePrefix: string, - opts: CallRouteInterface - ) => Promise>; - const superUser = { - username: 'superuser', - roles: ['superuser'], - }; + it('should accept an Endpoint ID as the only required field', () => { + expect(() => { + HostIsolationRequestSchema.body.validate({ + endpoint_ids: ['ABC-XYZ-000'], + }); + }).not.toThrow(); + }); - const docGen = new EndpointDocGenerator(); + it('should accept a comment', () => { + expect(() => { + HostIsolationRequestSchema.body.validate({ + endpoint_ids: ['ABC-XYZ-000'], + comment: 'a user comment', + }); + }).not.toThrow(); + }); - beforeEach(() => { - // instantiate... everything - const mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); - const mockClusterClient = elasticsearchServiceMock.createClusterClient(); - mockClusterClient.asScoped.mockReturnValue(mockScopedClient); - const routerMock = httpServiceMock.createRouter(); - mockResponse = httpServerMock.createResponseFactory(); - const startContract = createMockEndpointAppContextServiceStartContract(); - endpointAppContextService = new EndpointAppContextService(); - const mockSavedObjectClient = savedObjectsClientMock.create(); - const mockPackageService = createMockPackageService(); - mockPackageService.getInstalledEsAssetReferences.mockReturnValue( - Promise.resolve([ - { - id: 'logs-endpoint.events.security', - type: ElasticsearchAssetType.indexTemplate, - }, - { - id: `${metadataTransformPrefix}-0.16.0-dev.0`, - type: ElasticsearchAssetType.transform, - }, - ]) - ); - licenseEmitter = new Subject(); - licenseService = new LicenseService(); - licenseService.start(licenseEmitter); - endpointAppContextService.start({ - ...startContract, - licenseService, - packageService: mockPackageService, + it('should accept alert IDs', () => { + expect(() => { + HostIsolationRequestSchema.body.validate({ + endpoint_ids: ['ABC-XYZ-000'], + alert_ids: ['0000000-000-00'], + }); + }).not.toThrow(); }); - // add the host isolation route handlers to routerMock - registerHostIsolationRoutes(routerMock, { - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), + it('should accept case IDs', () => { + expect(() => { + HostIsolationRequestSchema.body.validate({ + endpoint_ids: ['ABC-XYZ-000'], + case_ids: ['000000000-000-000'], + }); + }).not.toThrow(); }); + }); + + describe('handler', () => { + let endpointAppContextService: EndpointAppContextService; + let mockResponse: jest.Mocked; + let licenseService: LicenseService; + let licenseEmitter: Subject; - // define a convenience function to execute an API call for a given route, body, and mocked response from ES - // it returns the requestContext mock used in the call, to assert internal calls (e.g. the indexed document) - callRoute = async ( + let callRoute: ( routePrefix: string, - { body, idxResponse, searchResponse, mockUser, license }: CallRouteInterface - ): Promise> => { - const asUser = mockUser ? mockUser : superUser; - (startContract.security.authc.getCurrentUser as jest.Mock).mockImplementationOnce( - () => asUser - ); - const ctx = createRouteHandlerContext(mockScopedClient, mockSavedObjectClient); - const withIdxResp = idxResponse ? idxResponse : { statusCode: 201 }; - ctx.core.elasticsearch.client.asCurrentUser.index = jest - .fn() - .mockImplementationOnce(() => Promise.resolve(withIdxResp)); - ctx.core.elasticsearch.client.asCurrentUser.search = jest - .fn() - .mockImplementationOnce(() => - Promise.resolve({ body: createV2SearchResponse(searchResponse) }) - ); - const withLicense = license ? license : Platinum; - licenseEmitter.next(withLicense); - const mockRequest = httpServerMock.createKibanaRequest({ body }); - const [, routeHandler]: [ - RouteConfig, - RequestHandler - ] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(routePrefix))!; - await routeHandler(ctx, mockRequest, mockResponse); - return (ctx as unknown) as jest.Mocked; + opts: CallRouteInterface + ) => Promise>; + const superUser = { + username: 'superuser', + roles: ['superuser'], }; - }); - - afterEach(() => { - endpointAppContextService.stop(); - licenseService.stop(); - licenseEmitter.complete(); - }); - it('errors if no endpoint or agent is provided', async () => { - await callRoute(ISOLATE_HOST_ROUTE, {}); - expect(mockResponse.badRequest).toBeCalled(); - }); - it('succeeds when an agent ID is provided', async () => { - await callRoute(ISOLATE_HOST_ROUTE, { body: { agent_ids: ['XYZ'] } }); - expect(mockResponse.ok).toBeCalled(); - }); - it('reports elasticsearch errors creating an action', async () => { - const ErrMessage = 'something went wrong?'; + const docGen = new EndpointDocGenerator(); - await callRoute(ISOLATE_HOST_ROUTE, { - body: { agent_ids: ['XYZ'] }, - idxResponse: { - statusCode: 500, - body: { - result: ErrMessage, - }, - }, - }); - expect(mockResponse.ok).not.toBeCalled(); - const response = mockResponse.customError.mock.calls[0][0]; - expect(response.statusCode).toEqual(500); - expect((response.body as Error).message).toEqual(ErrMessage); - }); - it('accepts a comment field', async () => { - await callRoute(ISOLATE_HOST_ROUTE, { body: { agent_ids: ['XYZ'], comment: 'XYZ' } }); - expect(mockResponse.ok).toBeCalled(); - }); - it('sends the action to the requested agent', async () => { - const AgentID = '123-ABC'; - const ctx = await callRoute(ISOLATE_HOST_ROUTE, { - body: { agent_ids: [AgentID] }, - }); - const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser - .index as jest.Mock).mock.calls[0][0].body; - expect(actionDoc.agents).toContain(AgentID); - }); - it('records the user who performed the action to the action record', async () => { - const testU = { username: 'testuser', roles: ['superuser'] }; - const ctx = await callRoute(ISOLATE_HOST_ROUTE, { - body: { agent_ids: ['XYZ'] }, - mockUser: testU, - }); - const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser - .index as jest.Mock).mock.calls[0][0].body; - expect(actionDoc.user_id).toEqual(testU.username); - }); - it('records the comment in the action payload', async () => { - const CommentText = "I am isolating this because it's Friday"; - const ctx = await callRoute(ISOLATE_HOST_ROUTE, { - body: { agent_ids: ['XYZ'], comment: CommentText }, - }); - const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser - .index as jest.Mock).mock.calls[0][0].body; - expect(actionDoc.data.comment).toEqual(CommentText); - }); - it('creates an action and returns its ID', async () => { - const ctx = await callRoute(ISOLATE_HOST_ROUTE, { - body: { agent_ids: ['XYZ'], comment: 'XYZ' }, - }); - const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser - .index as jest.Mock).mock.calls[0][0].body; - const actionID = actionDoc.action_id; - expect(mockResponse.ok).toBeCalled(); - expect((mockResponse.ok.mock.calls[0][0]?.body as HostIsolationResponse).action).toEqual( - actionID - ); - }); - - it('succeeds when just an endpoint ID is provided', async () => { - await callRoute(ISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'] } }); - expect(mockResponse.ok).toBeCalled(); - }); - it('sends the action to the correct agent when endpoint ID is given', async () => { - const doc = docGen.generateHostMetadata(); - const AgentID = doc.elastic.agent.id; + beforeEach(() => { + // instantiate... everything + const mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockClusterClient = elasticsearchServiceMock.createClusterClient(); + mockClusterClient.asScoped.mockReturnValue(mockScopedClient); + const routerMock = httpServiceMock.createRouter(); + mockResponse = httpServerMock.createResponseFactory(); + const startContract = createMockEndpointAppContextServiceStartContract(); + endpointAppContextService = new EndpointAppContextService(); + const mockSavedObjectClient = savedObjectsClientMock.create(); + const mockPackageService = createMockPackageService(); + mockPackageService.getInstalledEsAssetReferences.mockReturnValue( + Promise.resolve([ + { + id: 'logs-endpoint.events.security', + type: ElasticsearchAssetType.indexTemplate, + }, + { + id: `${metadataTransformPrefix}-0.16.0-dev.0`, + type: ElasticsearchAssetType.transform, + }, + ]) + ); + licenseEmitter = new Subject(); + licenseService = new LicenseService(); + licenseService.start(licenseEmitter); + endpointAppContextService.start({ + ...startContract, + licenseService, + packageService: mockPackageService, + }); - const ctx = await callRoute(ISOLATE_HOST_ROUTE, { - body: { endpoint_ids: ['XYZ'] }, - searchResponse: doc, - }); - const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser - .index as jest.Mock).mock.calls[0][0].body; - expect(actionDoc.agents).toContain(AgentID); - }); - it('combines given agent IDs and endpoint IDs', async () => { - const doc = docGen.generateHostMetadata(); - const explicitAgentID = 'XYZ'; - const lookupAgentID = doc.elastic.agent.id; + // add the host isolation route handlers to routerMock + registerHostIsolationRoutes(routerMock, { + logFactory: loggingSystemMock.create(), + service: endpointAppContextService, + config: () => Promise.resolve(createMockConfig()), + experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), + }); - const ctx = await callRoute(ISOLATE_HOST_ROUTE, { - body: { agent_ids: [explicitAgentID], endpoint_ids: ['XYZ'] }, - searchResponse: doc, + // define a convenience function to execute an API call for a given route, body, and mocked response from ES + // it returns the requestContext mock used in the call, to assert internal calls (e.g. the indexed document) + callRoute = async ( + routePrefix: string, + { body, idxResponse, searchResponse, mockUser, license }: CallRouteInterface + ): Promise> => { + const asUser = mockUser ? mockUser : superUser; + (startContract.security.authc.getCurrentUser as jest.Mock).mockImplementationOnce( + () => asUser + ); + const ctx = createRouteHandlerContext(mockScopedClient, mockSavedObjectClient); + const withIdxResp = idxResponse ? idxResponse : { statusCode: 201 }; + ctx.core.elasticsearch.client.asCurrentUser.index = jest + .fn() + .mockImplementationOnce(() => Promise.resolve(withIdxResp)); + ctx.core.elasticsearch.client.asCurrentUser.search = jest + .fn() + .mockImplementation(() => + Promise.resolve({ body: createV2SearchResponse(searchResponse) }) + ); + const withLicense = license ? license : Platinum; + licenseEmitter.next(withLicense); + const mockRequest = httpServerMock.createKibanaRequest({ body }); + const [, routeHandler]: [ + RouteConfig, + RequestHandler + ] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(routePrefix))!; + await routeHandler(ctx, mockRequest, mockResponse); + return (ctx as unknown) as jest.Mocked; + }; }); - const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser - .index as jest.Mock).mock.calls[0][0].body; - expect(actionDoc.agents).toContain(explicitAgentID); - expect(actionDoc.agents).toContain(lookupAgentID); - }); - it('sends the isolate command payload from the isolate route', async () => { - const ctx = await callRoute(ISOLATE_HOST_ROUTE, { - body: { agent_ids: ['XYZ'] }, + afterEach(() => { + endpointAppContextService.stop(); + licenseService.stop(); + licenseEmitter.complete(); }); - const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser - .index as jest.Mock).mock.calls[0][0].body; - expect(actionDoc.data.command).toEqual('isolate'); - }); - it('sends the unisolate command payload from the unisolate route', async () => { - const ctx = await callRoute(UNISOLATE_HOST_ROUTE, { - body: { agent_ids: ['XYZ'] }, + it('succeeds when an endpoint ID is provided', async () => { + await callRoute(ISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'] } }); + expect(mockResponse.ok).toBeCalled(); }); - const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser - .index as jest.Mock).mock.calls[0][0].body; - expect(actionDoc.data.command).toEqual('unisolate'); - }); + it('reports elasticsearch errors creating an action', async () => { + const ErrMessage = 'something went wrong?'; - describe('License Level', () => { - it('allows platinum license levels to isolate hosts', async () => { await callRoute(ISOLATE_HOST_ROUTE, { - body: { agent_ids: ['XYZ'] }, - license: Platinum, + body: { endpoint_ids: ['XYZ'] }, + idxResponse: { + statusCode: 500, + body: { + result: ErrMessage, + }, + }, }); + expect(mockResponse.ok).not.toBeCalled(); + const response = mockResponse.customError.mock.calls[0][0]; + expect(response.statusCode).toEqual(500); + expect((response.body as Error).message).toEqual(ErrMessage); + }); + it('accepts a comment field', async () => { + await callRoute(ISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'], comment: 'XYZ' } }); expect(mockResponse.ok).toBeCalled(); }); - it('prohibits license levels less than platinum from isolating hosts', async () => { - licenseEmitter.next(Gold); - await callRoute(ISOLATE_HOST_ROUTE, { - body: { agent_ids: ['XYZ'] }, - license: Gold, + it('sends the action to the requested agent', async () => { + const metadataResponse = docGen.generateHostMetadata(); + const AgentID = metadataResponse.elastic.agent.id; + const ctx = await callRoute(ISOLATE_HOST_ROUTE, { + body: { endpoint_ids: ['ABC-XYZ-000'] }, + searchResponse: metadataResponse, }); - expect(mockResponse.forbidden).toBeCalled(); + const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser + .index as jest.Mock).mock.calls[0][0].body; + expect(actionDoc.agents).toContain(AgentID); }); - it('allows any license level to unisolate', async () => { - licenseEmitter.next(Gold); - await callRoute(UNISOLATE_HOST_ROUTE, { - body: { agent_ids: ['XYZ'] }, - license: Gold, + it('records the user who performed the action to the action record', async () => { + const testU = { username: 'testuser', roles: ['superuser'] }; + const ctx = await callRoute(ISOLATE_HOST_ROUTE, { + body: { endpoint_ids: ['XYZ'] }, + mockUser: testU, }); + const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser + .index as jest.Mock).mock.calls[0][0].body; + expect(actionDoc.user_id).toEqual(testU.username); + }); + it('records the comment in the action payload', async () => { + const CommentText = "I am isolating this because it's Friday"; + const ctx = await callRoute(ISOLATE_HOST_ROUTE, { + body: { endpoint_ids: ['XYZ'], comment: CommentText }, + }); + const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser + .index as jest.Mock).mock.calls[0][0].body; + expect(actionDoc.data.comment).toEqual(CommentText); + }); + it('creates an action and returns its ID', async () => { + const ctx = await callRoute(ISOLATE_HOST_ROUTE, { + body: { endpoint_ids: ['XYZ'], comment: 'XYZ' }, + }); + const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser + .index as jest.Mock).mock.calls[0][0].body; + const actionID = actionDoc.action_id; expect(mockResponse.ok).toBeCalled(); + expect((mockResponse.ok.mock.calls[0][0]?.body as HostIsolationResponse).action).toEqual( + actionID + ); }); - }); - describe('User Level', () => { - it('allows superuser to perform isolation', async () => { - const superU = { username: 'foo', roles: ['superuser'] }; - await callRoute(ISOLATE_HOST_ROUTE, { - body: { agent_ids: ['XYZ'] }, - mockUser: superU, - }); + it('succeeds when just an endpoint ID is provided', async () => { + await callRoute(ISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'] } }); expect(mockResponse.ok).toBeCalled(); }); - it('allows superuser to perform unisolation', async () => { - const superU = { username: 'foo', roles: ['superuser'] }; - await callRoute(UNISOLATE_HOST_ROUTE, { - body: { agent_ids: ['XYZ'] }, - mockUser: superU, + it('sends the action to the correct agent when endpoint ID is given', async () => { + const doc = docGen.generateHostMetadata(); + const AgentID = doc.elastic.agent.id; + + const ctx = await callRoute(ISOLATE_HOST_ROUTE, { + body: { endpoint_ids: ['XYZ'] }, + searchResponse: doc, }); - expect(mockResponse.ok).toBeCalled(); + const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser + .index as jest.Mock).mock.calls[0][0].body; + expect(actionDoc.agents).toContain(AgentID); }); - it('prohibits non-admin user from performing isolation', async () => { - const superU = { username: 'foo', roles: ['user'] }; - await callRoute(ISOLATE_HOST_ROUTE, { - body: { agent_ids: ['XYZ'] }, - mockUser: superU, + it('sends the isolate command payload from the isolate route', async () => { + const ctx = await callRoute(ISOLATE_HOST_ROUTE, { + body: { endpoint_ids: ['XYZ'] }, }); - expect(mockResponse.forbidden).toBeCalled(); + const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser + .index as jest.Mock).mock.calls[0][0].body; + expect(actionDoc.data.command).toEqual('isolate'); }); - it('prohibits non-admin user from performing unisolation', async () => { - const superU = { username: 'foo', roles: ['user'] }; - await callRoute(UNISOLATE_HOST_ROUTE, { - body: { agent_ids: ['XYZ'] }, - mockUser: superU, + it('sends the unisolate command payload from the unisolate route', async () => { + const ctx = await callRoute(UNISOLATE_HOST_ROUTE, { + body: { endpoint_ids: ['XYZ'] }, + }); + const actionDoc: EndpointAction = (ctx.core.elasticsearch.client.asCurrentUser + .index as jest.Mock).mock.calls[0][0].body; + expect(actionDoc.data.command).toEqual('unisolate'); + }); + + describe('License Level', () => { + it('allows platinum license levels to isolate hosts', async () => { + await callRoute(ISOLATE_HOST_ROUTE, { + body: { endpoint_ids: ['XYZ'] }, + license: Platinum, + }); + expect(mockResponse.ok).toBeCalled(); + }); + it('prohibits license levels less than platinum from isolating hosts', async () => { + licenseEmitter.next(Gold); + await callRoute(ISOLATE_HOST_ROUTE, { + body: { endpoint_ids: ['XYZ'] }, + license: Gold, + }); + expect(mockResponse.forbidden).toBeCalled(); + }); + it('allows any license level to unisolate', async () => { + licenseEmitter.next(Gold); + await callRoute(UNISOLATE_HOST_ROUTE, { + body: { endpoint_ids: ['XYZ'] }, + license: Gold, + }); + expect(mockResponse.ok).toBeCalled(); }); - expect(mockResponse.forbidden).toBeCalled(); }); - }); - describe('Cases', () => { - it.todo('logs a comment to the provided case'); - it.todo('logs a comment to any cases associated with the given alert'); + describe('User Level', () => { + it('allows superuser to perform isolation', async () => { + const superU = { username: 'foo', roles: ['superuser'] }; + await callRoute(ISOLATE_HOST_ROUTE, { + body: { endpoint_ids: ['XYZ'] }, + mockUser: superU, + }); + expect(mockResponse.ok).toBeCalled(); + }); + it('allows superuser to perform unisolation', async () => { + const superU = { username: 'foo', roles: ['superuser'] }; + await callRoute(UNISOLATE_HOST_ROUTE, { + body: { endpoint_ids: ['XYZ'] }, + mockUser: superU, + }); + expect(mockResponse.ok).toBeCalled(); + }); + + it('prohibits non-admin user from performing isolation', async () => { + const superU = { username: 'foo', roles: ['user'] }; + await callRoute(ISOLATE_HOST_ROUTE, { + body: { endpoint_ids: ['XYZ'] }, + mockUser: superU, + }); + expect(mockResponse.forbidden).toBeCalled(); + }); + it('prohibits non-admin user from performing unisolation', async () => { + const superU = { username: 'foo', roles: ['user'] }; + await callRoute(UNISOLATE_HOST_ROUTE, { + body: { endpoint_ids: ['XYZ'] }, + mockUser: superU, + }); + expect(mockResponse.forbidden).toBeCalled(); + }); + }); + + describe('Cases', () => { + it.todo('logs a comment to the provided case'); + it.todo('logs a comment to any cases associated with the given alert'); + }); }); }); 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 785434aa17ec65..45063ca92e2b0a 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 @@ -14,12 +14,12 @@ import { CasesByAlertId } from '../../../../../cases/common/api/cases/case'; import { HostIsolationRequestSchema } from '../../../../common/endpoint/schema/actions'; import { ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE } from '../../../../common/endpoint/constants'; import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common'; -import { EndpointAction } from '../../../../common/endpoint/types'; +import { EndpointAction, HostMetadata } from '../../../../common/endpoint/types'; import { SecuritySolutionPluginRouter, SecuritySolutionRequestHandlerContext, } from '../../../types'; -import { getAgentIDsForEndpoints } from '../../services'; +import { getMetadataForEndpoints } from '../../services'; import { EndpointAppContext } from '../../types'; import { APP_ID } from '../../../../common/constants'; import { userCanIsolate } from '../../../../common/endpoint/actions'; @@ -61,19 +61,7 @@ export const isolationRequestHandler = function ( TypeOf, SecuritySolutionRequestHandlerContext > { - // eslint-disable-next-line complexity return async (context, req, res) => { - if ( - (!req.body.agent_ids || req.body.agent_ids.length === 0) && - (!req.body.endpoint_ids || req.body.endpoint_ids.length === 0) - ) { - return res.badRequest({ - body: { - message: 'At least one agent ID or endpoint ID is required', - }, - }); - } - // only allow admin users const user = endpointContext.service.security?.authc.getCurrentUser(req); if (!userCanIsolate(user?.roles)) { @@ -93,13 +81,9 @@ export const isolationRequestHandler = function ( }); } - // translate any endpoint_ids into agent_ids - let agentIDs = req.body.agent_ids?.slice() || []; - if (req.body.endpoint_ids && req.body.endpoint_ids.length > 0) { - const newIDs = await getAgentIDsForEndpoints(req.body.endpoint_ids, context, endpointContext); - agentIDs = agentIDs.concat(newIDs); - } - agentIDs = [...new Set(agentIDs)]; // dedupe + // 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 casesClient = await endpointContext.service.getCasesClient(req); @@ -134,7 +118,7 @@ export const isolationRequestHandler = function ( expiration: moment().add(2, 'weeks').toISOString(), type: 'INPUT_ACTION', input_type: 'endpoint', - agents: agentIDs, + agents: endpointData.map((endpt: HostMetadata) => endpt.elastic.agent.id), user_id: user!.username, data: { command: isolate ? 'isolate' : 'unisolate', @@ -158,25 +142,24 @@ export const isolationRequestHandler = function ( }); } - const commentLines: string[] = []; - - commentLines.push(`${isolate ? 'I' : 'Uni'}solate action was sent to the following Agents:`); - // lines of markdown links, inside a code block - - commentLines.push(`${agentIDs.map((a) => `- [${a}](/app/fleet#/agents/${a})`).join('\n')}`); - if (req.body.comment) { - commentLines.push(`\n\nWith Comment:\n> ${req.body.comment}`); - } - // Update all cases with a comment if (caseIDs.length > 0) { + const targets = endpointData.map((endpt: HostMetadata) => ({ + hostname: endpt.host.hostname, + endpointId: endpt.agent.id, + })); + await Promise.all( caseIDs.map((caseId) => casesClient.attachments.add({ caseId, comment: { - comment: commentLines.join('\n'), - type: CommentType.user, + type: CommentType.actions, + comment: req.body.comment || '', + actions: { + targets, + type: isolate ? 'isolate' : 'unisolate', + }, owner: APP_ID, }, }) diff --git a/x-pack/plugins/security_solution/server/endpoint/services/index.ts b/x-pack/plugins/security_solution/server/endpoint/services/index.ts index 9fabd043e2950d..8bf64999c746a1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/index.ts @@ -6,4 +6,4 @@ */ export * from './artifacts'; -export { getAgentIDsForEndpoints } from './lookup_agent'; +export { getMetadataForEndpoints } from './metadata'; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/lookup_agent.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata.ts similarity index 89% rename from x-pack/plugins/security_solution/server/endpoint/services/lookup_agent.ts rename to x-pack/plugins/security_solution/server/endpoint/services/metadata.ts index e82b548641290e..0ca1983aa68d51 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/lookup_agent.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata.ts @@ -12,11 +12,11 @@ import { SecuritySolutionRequestHandlerContext } from '../../types'; import { getESQueryHostMetadataByIDs } from '../routes/metadata/query_builders'; import { EndpointAppContext } from '../types'; -export async function getAgentIDsForEndpoints( +export async function getMetadataForEndpoints( endpointIDs: string[], requestHandlerContext: SecuritySolutionRequestHandlerContext, endpointAppContext: EndpointAppContext -): Promise { +): Promise { const queryStrategy = await endpointAppContext.service ?.getMetadataService() ?.queryStrategy(requestHandlerContext.core.savedObjects.client); @@ -25,6 +25,5 @@ export async function getAgentIDsForEndpoints( const esClient = requestHandlerContext.core.elasticsearch.client.asCurrentUser; const { body } = await esClient.search(query as SearchRequest); const hosts = queryStrategy!.queryResponseToHostListResult(body as SearchResponse); - - return hosts.resultList.map((x: HostMetadata): string => x.elastic.agent.id); + return hosts.resultList; } From 1ff2407b185b71f969f567369062c8a5b9a3e75c Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Tue, 29 Jun 2021 19:46:19 -0400 Subject: [PATCH 012/121] [Security Solution] Adds a version and OS check for Host Isolation (#103026) --- .../data_generators/base_data_generator.ts | 2 +- .../common/endpoint/generate_data.ts | 8 +-- .../service/host_isolation/utils.test.ts | 66 +++++++++++++++++++ .../endpoint/service/host_isolation/utils.ts | 46 +++++++++++++ .../details/components/actions_menu.test.tsx | 4 ++ .../view/hooks/use_endpoint_action_items.tsx | 7 +- .../pages/endpoint_hosts/view/index.test.tsx | 11 ++++ .../side_panel/event_details/index.tsx | 38 ++++++++--- 8 files changed, 167 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/service/host_isolation/utils.test.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/service/host_isolation/utils.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts index 1f3d4307197f8a..1c9adc8f2f9c39 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts @@ -102,7 +102,7 @@ export class BaseDataGenerator { } protected randomVersion(): string { - return [6, ...this.randomNGenerator(10, 2)].map((x) => x.toString()).join('.'); + return [7, ...this.randomNGenerator(20, 2)].map((x) => x.toString()).join('.'); } protected randomChoice(choices: T[]): T { diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index b08d5649540db7..876cb3866c6147 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -51,7 +51,7 @@ export const ANCESTRY_LIMIT: number = 2; const Windows: OSFields[] = [ { - name: 'windows 10.0', + name: 'Windows', full: 'Windows 10', version: '10.0', platform: 'Windows', @@ -61,7 +61,7 @@ const Windows: OSFields[] = [ }, }, { - name: 'windows 10.0', + name: 'Windows', full: 'Windows Server 2016', version: '10.0', platform: 'Windows', @@ -71,7 +71,7 @@ const Windows: OSFields[] = [ }, }, { - name: 'windows 6.2', + name: 'Windows', full: 'Windows Server 2012', version: '6.2', platform: 'Windows', @@ -81,7 +81,7 @@ const Windows: OSFields[] = [ }, }, { - name: 'windows 6.3', + name: 'Windows', full: 'Windows Server 2012R2', version: '6.3', platform: 'Windows', diff --git a/x-pack/plugins/security_solution/common/endpoint/service/host_isolation/utils.test.ts b/x-pack/plugins/security_solution/common/endpoint/service/host_isolation/utils.test.ts new file mode 100644 index 00000000000000..7d3810bed8f44f --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/host_isolation/utils.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isVersionSupported, isOsSupported, isIsolationSupported } from './utils'; + +describe('Host Isolation utils isVersionSupported', () => { + test.each` + a | b | expected + ${'8.14.0'} | ${'7.13.0'} | ${true} + ${'7.14.0'} | ${'7.13.0'} | ${true} + ${'7.14.1'} | ${'7.14.0'} | ${true} + ${'8.14.0'} | ${'9.14.0'} | ${false} + ${'7.13.0'} | ${'7.14.0'} | ${false} + ${'7.14.0'} | ${'7.14.1'} | ${false} + ${'7.14.0'} | ${'7.14.0'} | ${true} + ${'7.14.0-SNAPSHOT'} | ${'7.14.0'} | ${true} + ${'7.14.0-SNAPSHOT-beta'} | ${'7.14.0'} | ${true} + ${'7.14.0-alpha'} | ${'7.14.0'} | ${true} + `('should validate that version $a is compatible($expected) to $b', ({ a, b, expected }) => { + expect( + isVersionSupported({ + currentVersion: a, + minVersionRequired: b, + }) + ).toEqual(expected); + }); +}); + +describe('Host Isolation utils isOsSupported', () => { + test.each` + a | b | expected + ${'linux'} | ${['macos', 'linux']} | ${true} + ${'linux'} | ${['macos', 'windows']} | ${false} + `('should validate that os $a is compatible($expected) to $b', ({ a, b, expected }) => { + expect( + isOsSupported({ + currentOs: a, + supportedOss: b, + }) + ).toEqual(expected); + }); +}); + +describe('Host Isolation utils isIsolationSupported', () => { + test.each` + a | b | expected + ${'windows'} | ${'7.14.0'} | ${true} + ${'linux'} | ${'7.13.0'} | ${false} + ${'linux'} | ${'7.14.0'} | ${false} + ${'macos'} | ${'7.13.0'} | ${false} + `( + 'should validate that os $a and version $b supports hostIsolation($expected)', + ({ a, b, expected }) => { + expect( + isIsolationSupported({ + osName: a, + version: b, + }) + ).toEqual(expected); + } + ); +}); diff --git a/x-pack/plugins/security_solution/common/endpoint/service/host_isolation/utils.ts b/x-pack/plugins/security_solution/common/endpoint/service/host_isolation/utils.ts new file mode 100644 index 00000000000000..c5e57179bcb8d3 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/host_isolation/utils.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const isVersionSupported = ({ + currentVersion, + minVersionRequired, +}: { + currentVersion: string; + minVersionRequired: string; +}) => { + const parsedCurrentVersion = currentVersion.includes('-SNAPSHOT') + ? currentVersion.substring(0, currentVersion.indexOf('-')) + : currentVersion; + const tokenizedCurrent = parsedCurrentVersion + .split('.') + .map((token: string) => parseInt(token, 10)); + const tokenizedMin = minVersionRequired.split('.').map((token: string) => parseInt(token, 10)); + + const versionNotSupported = tokenizedCurrent.some((token: number, index: number) => { + return token < tokenizedMin[index]; + }); + + return !versionNotSupported; +}; + +export const isOsSupported = ({ + currentOs, + supportedOss, +}: { + currentOs: string; + supportedOss: string[]; +}) => { + return supportedOss.some((os) => currentOs === os); +}; + +export const isIsolationSupported = ({ osName, version }: { osName: string; version: string }) => { + const normalizedOs = osName.toLowerCase(); + return ( + isOsSupported({ currentOs: normalizedOs, supportedOss: ['macos', 'windows'] }) && + isVersionSupported({ currentVersion: version, minVersionRequired: '7.14.0' }) + ); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx index 04fd8cd715c879..56ff523e2c887b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx @@ -46,6 +46,10 @@ describe('When using the Endpoint Details Actions Menu', () => { // Safe to mutate this mocked data // @ts-ignore endpointHost.metadata.Endpoint.state.isolation = isolation; + // @ts-ignore + endpointHost.metadata.host.os.name = 'Windows'; + // @ts-ignore + endpointHost.metadata.agent.version = '7.14.0'; httpMocks.responseProvider.metadataDetails.mockReturnValue(endpointHost); }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx index 0422cbcaa4310b..584e6df1ff7818 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx @@ -17,6 +17,7 @@ import { useAppUrl } from '../../../../../common/lib/kibana/hooks'; import { ContextMenuItemNavByRouterProps } from '../components/context_menu_item_nav_by_rotuer'; import { isEndpointHostIsolated } from '../../../../../common/utils/validators'; import { useLicense } from '../../../../../common/hooks/use_license'; +import { isIsolationSupported } from '../../../../../../common/endpoint/service/host_isolation/utils'; /** * Returns a list (array) of actions for an individual endpoint @@ -37,6 +38,10 @@ export const useEndpointActionItems = ( const endpointPolicyId = endpointMetadata.Endpoint.policy.applied.id; const endpointHostName = endpointMetadata.host.hostname; const fleetAgentId = endpointMetadata.elastic.agent.id; + const isolationSupported = isIsolationSupported({ + osName: endpointMetadata.host.os.name, + version: endpointMetadata.agent.version, + }); const { show, selected_endpoint: _selectedEndpoint, @@ -73,7 +78,7 @@ export const useEndpointActionItems = ( /> ), }); - } else if (isPlatinumPlus) { + } else if (isPlatinumPlus && isolationSupported) { // For Platinum++ licenses, users also have ability to isolate isolationActions.push({ 'data-test-subj': 'isolateLink', 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 7bfd77e7dd9754..ee5ef52d00f184 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 @@ -1189,6 +1189,17 @@ describe('when on the endpoint list page', () => { isolation: false, }, }, + host: { + ...hosts[0].metadata.host, + os: { + ...hosts[0].metadata.host.os, + name: 'Windows', + }, + }, + agent: { + ...hosts[0].metadata.agent, + version: '7.14.0', + }, }, query_strategy_version: queryStrategyVersion, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index 58b991dafed67f..509c629dc287c8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -32,6 +32,7 @@ import { } from '../../../../detections/components/host_isolation/translations'; import { ALERT_DETAILS } from './translations'; import { useIsolationPrivileges } from '../../../../common/hooks/endpoint/use_isolate_privileges'; +import { isIsolationSupported } from '../../../../../common/endpoint/service/host_isolation/utils'; import { endpointAlertCheck } from '../../../../common/utils/endpoint_alert_check'; import { useWithCaseDetailsRefresh } from '../../../../common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context'; @@ -102,6 +103,22 @@ const EventDetailsPanelComponent: React.FC = ({ return findAgentId ? findAgentId[0] : ''; }, [detailsData]); + const hostOsFamily = useMemo(() => { + const findOsName = find({ category: 'host', field: 'host.os.name' }, detailsData)?.values; + return findOsName ? findOsName[0] : ''; + }, [detailsData]); + + const agentVersion = useMemo(() => { + const findAgentVersion = find({ category: 'agent', field: 'agent.version' }, detailsData) + ?.values; + return findAgentVersion ? findAgentVersion[0] : ''; + }, [detailsData]); + + const isolationSupported = isIsolationSupported({ + osName: hostOsFamily, + version: agentVersion, + }); + const backToAlertDetailsLink = useMemo(() => { return ( <> @@ -164,15 +181,18 @@ const EventDetailsPanelComponent: React.FC = ({ /> )} - {isIsolationAllowed && isEndpointAlert && isHostIsolationPanelOpen === false && ( - - - - - - - - )} + {isIsolationAllowed && + isEndpointAlert && + isolationSupported && + isHostIsolationPanelOpen === false && ( + + + + + + + + )} ) : ( <> From fee73488066583577dd98138115acb284926a0df Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Tue, 29 Jun 2021 20:02:38 -0400 Subject: [PATCH 013/121] [Security Solution][Exceptions Lists] Adds overflow container for exceptions list table (#103377) --- .../rules/all/exceptions/columns.tsx | 27 +--- .../exceptions_overflow_display.tsx | 119 ++++++++++++++++++ 2 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_overflow_display.tsx diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx index c4c938d5bb05ec..582ca0252604c6 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx @@ -11,12 +11,10 @@ import React from 'react'; import { EuiButtonIcon, EuiBasicTableColumn, EuiToolTip } from '@elastic/eui'; import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; -import { Spacer } from '../../../../../../common/components/page'; import { FormatUrl } from '../../../../../../common/components/link_to'; -import { LinkAnchor } from '../../../../../../common/components/links'; import * as i18n from './translations'; import { ExceptionListInfo } from './use_all_exception_lists'; -import { getRuleDetailsUrl } from '../../../../../../common/components/link_to/redirect_to_detection_engine'; +import { ExceptionOverflowDisplay } from './exceptions_overflow_display'; export type AllExceptionListsColumns = EuiBasicTableColumn; @@ -72,24 +70,11 @@ export const getAllExceptionListsColumns = ( width: '20%', render: (value: ExceptionListInfo['rules']) => { return ( - <> - {value.map(({ id, name }, index) => ( - - void }) => { - ev.preventDefault(); - navigateToUrl(formatUrl(getRuleDetailsUrl(id))); - }} - href={formatUrl(getRuleDetailsUrl(id))} - > - {name} - - {index !== value.length - 1 ? ', ' : ''} - - ))} - + ); }, }, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_overflow_display.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_overflow_display.tsx new file mode 100644 index 00000000000000..d22d338d6f50a2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_overflow_display.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { EuiPopover, EuiBadgeGroup, EuiButton } from '@elastic/eui'; +import styled from 'styled-components'; +import * as i18n from '../../translations'; +import { Spacer } from '../../../../../../common/components/page'; +import { LinkAnchor } from '../../../../../../common/components/links'; +import { getRuleDetailsUrl } from '../../../../../../common/components/link_to/redirect_to_detection_engine'; +import { Rule } from '../../../../../containers/detection_engine/rules'; +import { FormatUrl } from '../../../../../../common/components/link_to'; + +interface ExceptionOverflowDisplayProps { + rules: Rule[]; + navigateToUrl: (url: string) => Promise; + formatUrl: FormatUrl; +} + +interface OverflowListComponentProps { + rule: Rule; + index: number; +} + +const ExceptionOverflowWrapper = styled(EuiBadgeGroup)` + width: 100%; +`; + +const ExceptionOverflowPopoverWrapper = styled(EuiBadgeGroup)` + max-height: 200px; + max-width: 600px; + overflow: auto; +`; + +const ExceptionOverflowPopoverButton = styled(EuiButton)` + font-size: ${({ theme }) => theme.eui.euiFontSizeXS} + font-weight: 500; + height: 20px; +`; + +/** + * @param rules to display for filtering + */ +const ExceptionOverflowDisplayComponent = ({ + rules, + navigateToUrl, + formatUrl, +}: ExceptionOverflowDisplayProps) => { + const [isExceptionOverflowPopoverOpen, setIsExceptionOverflowPopoverOpen] = useState(false); + + const OverflowListComponent = ({ rule: { id, name }, index }: OverflowListComponentProps) => { + return ( + + void }) => { + ev.preventDefault(); + navigateToUrl(formatUrl(getRuleDetailsUrl(id))); + }} + href={formatUrl(getRuleDetailsUrl(id))} + > + {name} + + {index !== rules.length - 1 ? ', ' : ''} + + ); + }; + + return ( + <> + {rules.length <= 2 ? ( + + {rules.map((rule, index: number) => ( + + ))} + + ) : ( + + {rules.slice(0, 2).map((rule, index: number) => ( + + ))} + + setIsExceptionOverflowPopoverOpen(!isExceptionOverflowPopoverOpen)} + > + {i18n.COLUMN_SEE_ALL_POPOVER} + + } + isOpen={isExceptionOverflowPopoverOpen} + closePopover={() => setIsExceptionOverflowPopoverOpen(!isExceptionOverflowPopoverOpen)} + repositionOnScroll + > + + {rules.map((rule, index: number) => ( + + ))} + + + + )} + + ); +}; + +export const ExceptionOverflowDisplay = React.memo(ExceptionOverflowDisplayComponent); + +ExceptionOverflowDisplay.displayName = 'ExceptionOverflowDisplay'; From 25db1df1a3f399f1e7537ff9d186c152eca37223 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Tue, 29 Jun 2021 17:07:56 -0700 Subject: [PATCH 014/121] [Monitoring] Migrated legacy Elasticsearch client for 8.0 (#101850) --- .../server/es_client/instantiate_client.ts | 6 +- .../collectors/get_usage_collector.test.ts | 19 +- .../collectors/get_usage_collector.ts | 16 +- .../kibana_monitoring/collectors/index.ts | 6 +- .../collectors/lib/fetch_es_usage.test.ts | 110 ++++++------ .../collectors/lib/fetch_es_usage.ts | 34 +--- .../collectors/lib/fetch_license_type.test.ts | 41 +++-- .../collectors/lib/fetch_license_type.ts | 11 +- .../lib/fetch_stack_product_usage.test.ts | 99 +++++++---- .../lib/fetch_stack_product_usage.ts | 14 +- .../lib/get_stack_products_usage.test.ts | 15 +- .../lib/get_stack_products_usage.ts | 6 +- .../alerts/disable_watcher_cluster_alerts.ts | 17 +- .../lib/alerts/fetch_ccr_read_exceptions.ts | 2 +- .../server/lib/alerts/fetch_cluster_health.ts | 2 +- .../server/lib/alerts/fetch_clusters.ts | 4 +- .../alerts/fetch_cpu_usage_node_stats.test.ts | 2 +- .../lib/alerts/fetch_cpu_usage_node_stats.ts | 3 +- .../lib/alerts/fetch_disk_usage_node_stats.ts | 2 +- .../alerts/fetch_elasticsearch_versions.ts | 2 +- .../lib/alerts/fetch_index_shard_size.ts | 2 +- .../lib/alerts/fetch_kibana_versions.ts | 2 +- .../server/lib/alerts/fetch_licenses.ts | 2 +- .../lib/alerts/fetch_logstash_versions.ts | 2 +- .../alerts/fetch_memory_usage_node_stats.ts | 2 +- .../alerts/fetch_missing_monitoring_data.ts | 2 +- .../alerts/fetch_nodes_from_cluster_stats.ts | 2 +- .../fetch_thread_pool_rejections_stats.ts | 2 +- .../server/lib/apm/_get_time_of_last_event.ts | 2 +- .../monitoring/server/lib/apm/get_apm_info.ts | 4 +- .../monitoring/server/lib/apm/get_apms.ts | 4 +- .../server/lib/apm/get_apms_for_clusters.js | 4 +- .../monitoring/server/lib/apm/get_stats.js | 4 +- .../server/lib/beats/get_beat_summary.ts | 4 +- .../monitoring/server/lib/beats/get_beats.ts | 4 +- .../lib/beats/get_beats_for_clusters.js | 4 +- .../server/lib/beats/get_latest_stats.js | 4 +- .../monitoring/server/lib/beats/get_stats.js | 4 +- .../lib/cluster/flag_supported_clusters.ts | 4 +- .../server/lib/cluster/get_cluster_license.ts | 4 +- .../server/lib/cluster/get_clusters_state.ts | 4 +- .../server/lib/cluster/get_clusters_stats.ts | 4 +- .../server/lib/details/get_series.js | 2 +- .../server/lib/elasticsearch/ccr.ts | 4 +- .../lib/elasticsearch/get_last_recovery.ts | 2 +- .../server/lib/elasticsearch/get_ml_jobs.ts | 8 +- .../indices/get_index_summary.ts | 2 +- .../lib/elasticsearch/indices/get_indices.ts | 4 +- .../elasticsearch/nodes/get_node_summary.ts | 2 +- .../nodes/get_nodes/get_node_ids.js | 4 +- .../nodes/get_nodes/get_nodes.ts | 4 +- .../get_indices_unassigned_shard_stats.ts | 2 +- .../shards/get_nodes_shard_count.ts | 2 +- .../shards/get_shard_allocation.ts | 2 +- .../elasticsearch/shards/get_shard_stats.ts | 2 +- .../elasticsearch/verify_monitoring_auth.js | 2 +- .../lib/elasticsearch_settings/cluster.js | 11 +- .../server/lib/kibana/get_kibana_info.ts | 4 +- .../server/lib/kibana/get_kibanas.ts | 2 +- .../lib/kibana/get_kibanas_for_clusters.js | 2 +- .../server/lib/logs/get_log_types.ts | 4 +- .../monitoring/server/lib/logs/get_logs.ts | 4 +- .../lib/logstash/get_logstash_for_clusters.js | 2 +- .../server/lib/logstash/get_node_info.ts | 4 +- .../server/lib/logstash/get_nodes.ts | 2 +- .../server/lib/logstash/get_pipeline_ids.js | 4 +- .../logstash/get_pipeline_state_document.ts | 2 +- .../get_pipeline_stats_aggregation.js | 4 +- .../lib/logstash/get_pipeline_versions.js | 2 +- .../get_pipeline_vertex_stats_aggregation.js | 4 +- .../setup/collection/get_collection_status.js | 8 +- .../monitoring/server/license_service.ts | 14 +- x-pack/plugins/monitoring/server/plugin.ts | 162 ++++++++++-------- .../server/routes/api/v1/alerts/enable.ts | 2 +- .../server/routes/api/v1/elasticsearch/ccr.ts | 2 +- .../routes/api/v1/elasticsearch/ccr_shard.ts | 2 +- .../elasticsearch_settings/check/cluster.js | 1 + .../check/internal_monitoring.ts | 9 +- .../monitoring/server/static_globals.ts | 71 ++++++-- .../get_all_stats.test.ts | 25 ++- .../telemetry_collection/get_all_stats.ts | 4 +- .../get_beats_stats.test.ts | 24 +-- .../telemetry_collection/get_beats_stats.ts | 23 +-- .../get_cluster_uuids.test.ts | 33 ++-- .../telemetry_collection/get_cluster_uuids.ts | 18 +- .../telemetry_collection/get_es_stats.test.ts | 22 +-- .../telemetry_collection/get_es_stats.ts | 18 +- .../get_high_level_stats.test.ts | 20 ++- .../get_high_level_stats.ts | 27 +-- .../telemetry_collection/get_kibana_stats.ts | 4 +- .../telemetry_collection/get_licenses.test.ts | 20 ++- .../telemetry_collection/get_licenses.ts | 18 +- .../get_logstash_stats.test.ts | 45 +++-- .../get_logstash_stats.ts | 48 +++--- ...egister_monitoring_telemetry_collection.ts | 10 +- x-pack/plugins/monitoring/server/types.ts | 7 +- 96 files changed, 685 insertions(+), 526 deletions(-) diff --git a/x-pack/plugins/monitoring/server/es_client/instantiate_client.ts b/x-pack/plugins/monitoring/server/es_client/instantiate_client.ts index a32943dc10272a..58da336a5447dc 100644 --- a/x-pack/plugins/monitoring/server/es_client/instantiate_client.ts +++ b/x-pack/plugins/monitoring/server/es_client/instantiate_client.ts @@ -6,7 +6,7 @@ */ import { ConfigOptions } from 'elasticsearch'; -import { Logger, ILegacyCustomClusterClient } from 'kibana/server'; +import { Logger, ICustomClusterClient, ElasticsearchClientConfig } from 'kibana/server'; // @ts-ignore import { monitoringBulk } from '../kibana_monitoring/lib/monitoring_bulk'; import { monitoringEndpointDisableWatches } from './monitoring_endpoint_disable_watches'; @@ -25,8 +25,8 @@ export function instantiateClient( log: Logger, createClient: ( type: string, - clientConfig?: Partial - ) => ILegacyCustomClusterClient + clientConfig?: Partial | undefined + ) => ICustomClusterClient ) { const isMonitoringCluster = hasMonitoringCluster(elasticsearchConfig); const cluster = createClient('monitoring', { diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts index 03a3659b49ce19..9c72c1f8d3841b 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts @@ -6,11 +6,11 @@ */ import { getMonitoringUsageCollector } from './get_usage_collector'; -import { fetchClustersLegacy } from '../../lib/alerts/fetch_clusters'; +import { fetchClusters } from '../../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; jest.mock('../../lib/alerts/fetch_clusters', () => ({ - fetchClustersLegacy: jest.fn().mockImplementation(() => { + fetchClusters: jest.fn().mockImplementation(() => { return [ { clusterUuid: '1abc', @@ -59,7 +59,8 @@ jest.mock('./lib/fetch_license_type', () => ({ })); describe('getMonitoringUsageCollector', () => { - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); + const esClient = elasticsearchServiceMock.createClusterClient(); + const getEsClient = () => esClient; const config: any = { ui: { ccs: { @@ -72,7 +73,7 @@ describe('getMonitoringUsageCollector', () => { const usageCollection: any = { makeUsageCollector: jest.fn(), }; - await getMonitoringUsageCollector(usageCollection, config, esClient); + getMonitoringUsageCollector(usageCollection, config, getEsClient); const mock = (usageCollection.makeUsageCollector as jest.Mock).mock; @@ -122,7 +123,7 @@ describe('getMonitoringUsageCollector', () => { makeUsageCollector: jest.fn(), }; - await getMonitoringUsageCollector(usageCollection, config, esClient); + getMonitoringUsageCollector(usageCollection, config, getEsClient); const mock = (usageCollection.makeUsageCollector as jest.Mock).mock; const args = mock.calls[0]; @@ -149,11 +150,11 @@ describe('getMonitoringUsageCollector', () => { makeUsageCollector: jest.fn(), }; - await getMonitoringUsageCollector(usageCollection, config, esClient); + getMonitoringUsageCollector(usageCollection, config, getEsClient); const mock = (usageCollection.makeUsageCollector as jest.Mock).mock; const args = mock.calls[0]; - (fetchClustersLegacy as jest.Mock).mockImplementation(() => { + (fetchClusters as jest.Mock).mockImplementation(() => { return []; }); @@ -169,11 +170,11 @@ describe('getMonitoringUsageCollector', () => { makeUsageCollector: jest.fn(), }; - await getMonitoringUsageCollector(usageCollection, config, esClient); + getMonitoringUsageCollector(usageCollection, config, getEsClient); const mock = (usageCollection.makeUsageCollector as jest.Mock).mock; const args = mock.calls[0]; - (fetchClustersLegacy as jest.Mock).mockImplementation(() => { + (fetchClusters as jest.Mock).mockImplementation(() => { return []; }); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts index 6f638b6ff8f0e0..558a79e03dcb7a 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts @@ -6,20 +6,20 @@ */ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { ILegacyClusterClient } from 'src/core/server'; +import { IClusterClient } from 'src/core/server'; import { MonitoringConfig } from '../../config'; -import { fetchAvailableCcsLegacy } from '../../lib/alerts/fetch_available_ccs'; +import { fetchAvailableCcs } from '../../lib/alerts/fetch_available_ccs'; import { getStackProductsUsage } from './lib/get_stack_products_usage'; import { fetchLicenseType } from './lib/fetch_license_type'; import { MonitoringUsage, StackProductUsage, MonitoringClusterStackProductUsage } from './types'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../common/constants'; import { getCcsIndexPattern } from '../../lib/alerts/get_ccs_index_pattern'; -import { fetchClustersLegacy } from '../../lib/alerts/fetch_clusters'; +import { fetchClusters } from '../../lib/alerts/fetch_clusters'; export function getMonitoringUsageCollector( usageCollection: UsageCollectionSetup, config: MonitoringConfig, - legacyEsClient: ILegacyClusterClient + getClient: () => IClusterClient ) { return usageCollection.makeUsageCollector({ type: 'monitoring', @@ -103,12 +103,12 @@ export function getMonitoringUsageCollector( }, fetch: async ({ kibanaRequest }) => { const callCluster = kibanaRequest - ? legacyEsClient.asScoped(kibanaRequest).callAsCurrentUser - : legacyEsClient.callAsInternalUser; + ? getClient().asScoped(kibanaRequest).asCurrentUser + : getClient().asInternalUser; const usageClusters: MonitoringClusterStackProductUsage[] = []; - const availableCcs = config.ui.ccs.enabled ? await fetchAvailableCcsLegacy(callCluster) : []; + const availableCcs = config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : []; const elasticsearchIndex = getCcsIndexPattern(INDEX_PATTERN_ELASTICSEARCH, availableCcs); - const clusters = await fetchClustersLegacy(callCluster, elasticsearchIndex); + const clusters = await fetchClusters(callCluster, elasticsearchIndex); for (const cluster of clusters) { const license = await fetchLicenseType(callCluster, availableCcs, cluster.clusterUuid); const stackProducts = await getStackProductsUsage( diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts index 5549b7aa968d0b..06d47f00447286 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ILegacyClusterClient } from 'src/core/server'; +import { IClusterClient } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { getSettingsCollector } from './get_settings_collector'; import { getMonitoringUsageCollector } from './get_usage_collector'; @@ -16,10 +16,10 @@ export { KibanaSettingsCollector, getKibanaSettings } from './get_settings_colle export function registerCollectors( usageCollection: UsageCollectionSetup, config: MonitoringConfig, - legacyEsClient: ILegacyClusterClient + getClient: () => IClusterClient ) { usageCollection.registerCollector(getSettingsCollector(usageCollection, config)); usageCollection.registerCollector( - getMonitoringUsageCollector(usageCollection, config, legacyEsClient) + getMonitoringUsageCollector(usageCollection, config, getClient) ); } diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.test.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.test.ts index 6d01dbc120be37..b7d616a5a3cc77 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.test.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.test.ts @@ -5,41 +5,45 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { fetchESUsage } from './fetch_es_usage'; describe('fetchESUsage', () => { const clusterUuid = '1abcde2'; const index = '.monitoring-es-*'; - const callCluster = jest.fn().mockImplementation(() => ({ - hits: { - hits: [ - { - _source: { - cluster_stats: { - nodes: { - count: { - total: 10, + const callCluster = ({ + search: jest.fn().mockImplementation(() => ({ + body: { + hits: { + hits: [ + { + _source: { + cluster_stats: { + nodes: { + count: { + total: 10, + }, + }, }, }, }, - }, + ], }, - ], - }, - aggregations: { - indices: { - buckets: [ - { - key: '.monitoring-es-2', + aggregations: { + indices: { + buckets: [ + { + key: '.monitoring-es-2', + }, + ], }, - ], + }, }, - }, - })); - const config: any = {}; + })), + } as unknown) as ElasticsearchClient; it('should return usage data for Elasticsearch', async () => { - const result = await fetchESUsage(config, callCluster, clusterUuid, index); + const result = await fetchESUsage(callCluster, clusterUuid, index); expect(result).toStrictEqual({ count: 10, enabled: true, @@ -48,33 +52,37 @@ describe('fetchESUsage', () => { }); it('should handle some indices coming from Metricbeat', async () => { - const customCallCluster = jest.fn().mockImplementation(() => ({ - hits: { - hits: [ - { - _source: { - cluster_stats: { - nodes: { - count: { - total: 10, + const customCallCluster = ({ + search: jest.fn().mockImplementation(() => ({ + body: { + hits: { + hits: [ + { + _source: { + cluster_stats: { + nodes: { + count: { + total: 10, + }, + }, }, }, }, - }, + ], }, - ], - }, - aggregations: { - indices: { - buckets: [ - { - key: '.monitoring-es-mb-2', + aggregations: { + indices: { + buckets: [ + { + key: '.monitoring-es-mb-2', + }, + ], }, - ], + }, }, - }, - })); - const result = await fetchESUsage(config, customCallCluster, clusterUuid, index); + })), + } as unknown) as ElasticsearchClient; + const result = await fetchESUsage(customCallCluster, clusterUuid, index); expect(result).toStrictEqual({ count: 10, enabled: true, @@ -83,12 +91,16 @@ describe('fetchESUsage', () => { }); it('should handle no monitoring data', async () => { - const customCallCluster = jest.fn().mockImplementation(() => ({ - hits: { - hits: [], - }, - })); - const result = await fetchESUsage(config, customCallCluster, clusterUuid, index); + const customCallCluster = ({ + search: jest.fn().mockImplementation(() => ({ + body: { + hits: { + hits: [], + }, + }, + })), + } as unknown) as ElasticsearchClient; + const result = await fetchESUsage(customCallCluster, clusterUuid, index); expect(result).toStrictEqual({ count: 0, enabled: false, diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.ts index b96c119bf5ab8d..1fb2ba70f2ab11 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.ts @@ -5,30 +5,15 @@ * 2.0. */ -import { LegacyAPICaller } from 'src/core/server'; +import { ElasticsearchClient } from 'src/core/server'; import { get } from 'lodash'; -import { MonitoringConfig } from '../../../config'; +import { estypes } from '@elastic/elasticsearch'; import { StackProductUsage } from '../types'; -interface ESResponse { - hits: { - hits: ESResponseHits[]; - }; - aggregations: { - indices: { - buckets: ESIndicesBucket; - }; - }; -} - interface ESIndicesBucket { key: string; } -interface ESResponseHits { - _source: ClusterStats; -} - interface ClusterStats { cluster_stats: { nodes: { @@ -41,16 +26,15 @@ interface ClusterStats { } export async function fetchESUsage( - config: MonitoringConfig, - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, clusterUuid: string, index: string ): Promise { - const params = { + const params: estypes.SearchRequest = { index, size: 1, - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ 'hits.hits._source.cluster_stats.nodes.count.total', 'aggregations.indices.buckets', ], @@ -101,8 +85,8 @@ export async function fetchESUsage( }, }; - const response = await callCluster('search', params); - const esResponse = response as ESResponse; + const { body: response } = await callCluster.search(params); + const esResponse = response as estypes.SearchResponse; if (esResponse.hits.hits.length === 0) { return { count: 0, @@ -112,7 +96,7 @@ export async function fetchESUsage( } const hit = esResponse.hits.hits[0]._source; - const count = hit.cluster_stats.nodes.count.total; + const count = hit?.cluster_stats.nodes.count.total || 0; const buckets = get(esResponse, 'aggregations.indices.buckets', []) as ESIndicesBucket[]; const metricbeatUsed = Boolean(buckets.find((indexBucket) => indexBucket.key.includes('-mb-'))); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.test.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.test.ts index b5c3293af40209..e4d801cfac0c3a 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.test.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.test.ts @@ -5,24 +5,29 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { fetchLicenseType } from './fetch_license_type'; describe('fetchLicenseType', () => { const clusterUuid = '1abcde2'; const availableCcs: string[] = []; - const callCluster = jest.fn().mockImplementation(() => ({ - hits: { - hits: [ - { - _source: { - license: { - type: 'trial', + const callCluster = ({ + search: jest.fn().mockImplementation(() => ({ + body: { + hits: { + hits: [ + { + _source: { + license: { + type: 'trial', + }, + }, }, - }, + ], }, - ], - }, - })); + }, + })), + } as unknown) as ElasticsearchClient; it('should get the license type', async () => { const result = await fetchLicenseType(callCluster, availableCcs, clusterUuid); @@ -30,11 +35,15 @@ describe('fetchLicenseType', () => { }); it('should handle no license data', async () => { - const customCallCluster = jest.fn().mockImplementation(() => ({ - hits: { - hits: [], - }, - })); + const customCallCluster = ({ + search: jest.fn().mockImplementation(() => ({ + body: { + hits: { + hits: [], + }, + }, + })), + } as unknown) as ElasticsearchClient; const result = await fetchLicenseType(customCallCluster, availableCcs, clusterUuid); expect(result).toStrictEqual(null); }); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.ts index eed177a6647f20..f42623ff851ce7 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.ts @@ -6,12 +6,13 @@ */ import { get } from 'lodash'; -import { LegacyAPICaller } from 'src/core/server'; +import { ElasticsearchClient } from 'src/core/server'; +import { estypes } from '@elastic/elasticsearch'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../common/constants'; import { getCcsIndexPattern } from '../../../lib/alerts/get_ccs_index_pattern'; export async function fetchLicenseType( - callCluster: LegacyAPICaller, + client: ElasticsearchClient, availableCcs: string[], clusterUuid: string ) { @@ -19,9 +20,9 @@ export async function fetchLicenseType( if (availableCcs) { index = getCcsIndexPattern(index, availableCcs); } - const params = { + const params: estypes.SearchRequest = { index, - filterPath: ['hits.hits._source.license'], + filter_path: ['hits.hits._source.license'], body: { size: 1, sort: [ @@ -54,6 +55,6 @@ export async function fetchLicenseType( }, }, }; - const response = await callCluster('search', params); + const { body: response } = await client.search(params); return get(response, 'hits.hits[0]._source.license.type', null); } diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.test.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.test.ts index 4a2f1abdc96ef2..a8650c375a0dfc 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.test.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { fetchStackProductUsage } from './fetch_stack_product_usage'; describe('fetchStackProductUsage', () => { @@ -16,7 +17,27 @@ describe('fetchStackProductUsage', () => { }; it('should use appropiate query parameters', async () => { - const callCluster = jest.fn(); + const searchMock = jest.fn().mockImplementation(() => ({ + body: { + aggregations: { + uuids: { + buckets: [ + { + key: 'sadfsdf', + indices: { + buckets: [ + { + key: '.monitoring-kibana-8', + }, + ], + }, + }, + ], + }, + }, + }, + })); + const callCluster = ({ search: searchMock } as unknown) as ElasticsearchClient; await fetchStackProductUsage( config, callCluster, @@ -34,7 +55,7 @@ describe('fetchStackProductUsage', () => { }, ] ); - const params = callCluster.mock.calls[0][1]; + const params = searchMock.mock.calls[0][0]; expect(params.body.query.bool.must[0].term.type.value).toBe('kibana_stats'); expect(params.body.query.bool.must[1].term.cluster_uuid.value).toBe(clusterUuid); expect(params.body.query.bool.must[2].range.timestamp.gte).toBe('now-1h'); @@ -42,24 +63,28 @@ describe('fetchStackProductUsage', () => { }); it('should get the usage data', async () => { - const callCluster = jest.fn().mockImplementation(() => ({ - aggregations: { - uuids: { - buckets: [ - { - key: 'sadfsdf', - indices: { - buckets: [ - { - key: '.monitoring-kibana-8', + const callCluster = ({ + search: jest.fn().mockImplementation(() => ({ + body: { + aggregations: { + uuids: { + buckets: [ + { + key: 'sadfsdf', + indices: { + buckets: [ + { + key: '.monitoring-kibana-8', + }, + ], }, - ], - }, + }, + ], }, - ], + }, }, - }, - })); + })), + } as unknown) as ElasticsearchClient; const result = await fetchStackProductUsage( config, @@ -78,27 +103,31 @@ describe('fetchStackProductUsage', () => { }); it('should handle both collection types', async () => { - const callCluster = jest.fn().mockImplementation(() => ({ - aggregations: { - uuids: { - buckets: [ - { - key: 'sadfsdf', - indices: { - buckets: [ - { - key: '.monitoring-kibana-8', + const callCluster = ({ + search: jest.fn().mockImplementation(() => ({ + body: { + aggregations: { + uuids: { + buckets: [ + { + key: 'sadfsdf', + indices: { + buckets: [ + { + key: '.monitoring-kibana-8', + }, + { + key: '.monitoring-kibana-mb-8', + }, + ], }, - { - key: '.monitoring-kibana-mb-8', - }, - ], - }, + }, + ], }, - ], + }, }, - }, - })); + })), + } as unknown) as ElasticsearchClient; const result = await fetchStackProductUsage( config, diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts index 2160e159ad1008..527ed503c8fafc 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts @@ -6,7 +6,8 @@ */ import { get } from 'lodash'; -import { LegacyAPICaller } from 'src/core/server'; +import { ElasticsearchClient } from 'src/core/server'; +import { estypes } from '@elastic/elasticsearch'; import { MonitoringConfig } from '../../../config'; // @ts-ignore import { prefixIndexPattern } from '../../../lib/ccs_utils'; @@ -33,7 +34,7 @@ interface KeyBucket { export async function fetchStackProductUsage( config: MonitoringConfig, - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, clusterUuid: string, index: string, type: string, @@ -41,11 +42,11 @@ export async function fetchStackProductUsage( filters: any[] = [] ): Promise { const size = config.ui.max_bucket_size; - const params = { + const params: estypes.SearchRequest = { index, size: 0, - ignoreUnavailable: true, - filterPath: ['aggregations.uuids.buckets'], + ignore_unavailable: true, + filter_path: ['aggregations.uuids.buckets'], body: { query: { bool: { @@ -94,7 +95,8 @@ export async function fetchStackProductUsage( }, }; - const response = (await callCluster('search', params)) as ESResponse; + const { body: responseBody } = await callCluster.search(params); + const response = responseBody as estypes.SearchResponse; const uuidBuckets = get(response, 'aggregations.uuids.buckets', []) as UuidBucket[]; const count = uuidBuckets.length; const metricbeatUsed = Boolean( diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.test.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.test.ts index 8a3135ff832a98..928d7efcdd0d1c 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.test.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { getStackProductsUsage } from './get_stack_products_usage'; describe('getStackProductsUsage', () => { @@ -15,11 +16,15 @@ describe('getStackProductsUsage', () => { }; const clusterUuid = '1abcde2'; const availableCcs: string[] = []; - const callCluster = jest.fn().mockImplementation(() => ({ - hits: { - hits: [], - }, - })); + const callCluster = ({ + search: jest.fn().mockImplementation(() => ({ + body: { + hits: { + hits: [], + }, + }, + })), + } as unknown) as ElasticsearchClient; it('should get all stack products', async () => { const result = await getStackProductsUsage(config, callCluster, availableCcs, clusterUuid); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts index 0fd214f2fa8557..7cce1b392112f9 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LegacyAPICaller } from 'src/core/server'; +import { ElasticsearchClient } from 'src/core/server'; import { MonitoringClusterStackProductUsage } from '../types'; import { fetchESUsage } from './fetch_es_usage'; import { MonitoringConfig } from '../../../config'; @@ -24,7 +24,7 @@ import { getCcsIndexPattern } from '../../../lib/alerts/get_ccs_index_pattern'; export const getStackProductsUsage = async ( config: MonitoringConfig, - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, availableCcs: string[], clusterUuid: string ): Promise< @@ -38,7 +38,7 @@ export const getStackProductsUsage = async ( const logstashIndex = getCcsIndexPattern(INDEX_PATTERN_LOGSTASH, availableCcs); const beatsIndex = getCcsIndexPattern(INDEX_PATTERN_BEATS, availableCcs); const [elasticsearch, kibana, logstash, beats, apm] = await Promise.all([ - fetchESUsage(config, callCluster, clusterUuid, elasticsearchIndex), + fetchESUsage(callCluster, clusterUuid, elasticsearchIndex), fetchStackProductUsage( config, callCluster, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/disable_watcher_cluster_alerts.ts b/x-pack/plugins/monitoring/server/lib/alerts/disable_watcher_cluster_alerts.ts index dff828d05e2b60..e1dfb893869898 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/disable_watcher_cluster_alerts.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/disable_watcher_cluster_alerts.ts @@ -6,7 +6,7 @@ */ import { Logger } from 'kibana/server'; -import { LegacyAPICaller } from 'src/core/server'; +import { ElasticsearchClient } from 'src/core/server'; interface DisableWatchesResponse { exporters: Array< @@ -22,9 +22,13 @@ interface DisableWatchesResponse { >; } -async function callMigrationApi(callCluster: LegacyAPICaller, logger: Logger) { +async function callMigrationApi(callCluster: ElasticsearchClient, logger: Logger) { try { - return await callCluster('monitoring.disableWatches'); + const { body: response } = await callCluster.transport.request({ + method: 'post', + path: '/monitoring.disableWatches', + }); + return response as DisableWatchesResponse; } catch (err) { logger.warn( `Unable to call migration api to disable cluster alert watches. Message=${err.message}` @@ -33,8 +37,11 @@ async function callMigrationApi(callCluster: LegacyAPICaller, logger: Logger) { } } -export async function disableWatcherClusterAlerts(callCluster: LegacyAPICaller, logger: Logger) { - const response: DisableWatchesResponse = await callMigrationApi(callCluster, logger); +export async function disableWatcherClusterAlerts( + callCluster: ElasticsearchClient, + logger: Logger +) { + const response: DisableWatchesResponse | undefined = await callMigrationApi(callCluster, logger); if (!response || response.exporters.length === 0) { return true; } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts index a96a7454ea744a..560751d1297d53 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts @@ -18,7 +18,7 @@ export async function fetchCCRReadExceptions( ): Promise { const params = { index, - filterPath: ['aggregations.remote_clusters.buckets'], + filter_path: ['aggregations.remote_clusters.buckets'], body: { size: 0, query: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts index f622418c77910f..85bfbd9dbd0490 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts @@ -15,7 +15,7 @@ export async function fetchClusterHealth( ): Promise { const params = { index, - filterPath: [ + filter_path: [ 'hits.hits._source.cluster_state.status', 'hits.hits._source.cluster_uuid', 'hits.hits._index', diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts index bbaea8d9f206e6..8b5803d588e12e 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts @@ -23,7 +23,7 @@ export async function fetchClusters( ): Promise { const params = { index, - filterPath: [ + filter_path: [ 'hits.hits._source.cluster_settings.cluster.metadata.display_name', 'hits.hits._source.cluster_uuid', 'hits.hits._source.cluster_name', @@ -70,7 +70,7 @@ export async function fetchClustersLegacy( ): Promise { const params = { index, - filterPath: [ + filter_path: [ 'hits.hits._source.cluster_settings.cluster.metadata.display_name', 'hits.hits._source.cluster_uuid', 'hits.hits._source.cluster_name', diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts index 9cb773c81923b1..90cd456f18037a 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts @@ -204,7 +204,7 @@ describe('fetchCpuUsageNodeStats', () => { await fetchCpuUsageNodeStats(esClient, clusters, index, startMs, endMs, size); expect(params).toStrictEqual({ index: '.monitoring-es-*', - filterPath: ['aggregations'], + filter_path: ['aggregations'], body: { size: 0, query: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts index 07ca3572ad6b3f..6f7d27916a7b1b 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts @@ -34,10 +34,9 @@ export async function fetchCpuUsageNodeStats( // Using pure MS didn't seem to work well with the date_histogram interval // but minutes does const intervalInMinutes = moment.duration(endMs - startMs).asMinutes(); - const filterPath = ['aggregations']; const params = { index, - filterPath, + filter_path: ['aggregations'], body: { size: 0, query: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts index 2e8b5c7478e152..70f05991d42291 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts @@ -19,7 +19,7 @@ export async function fetchDiskUsageNodeStats( const clustersIds = clusters.map((cluster) => cluster.clusterUuid); const params = { index, - filterPath: ['aggregations'], + filter_path: ['aggregations'], body: { size: 0, query: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts index f25f1dbe594db9..f2f311ac870a51 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts @@ -16,7 +16,7 @@ export async function fetchElasticsearchVersions( ): Promise { const params = { index, - filterPath: [ + filter_path: [ 'hits.hits._source.cluster_stats.nodes.versions', 'hits.hits._index', 'hits.hits._source.cluster_uuid', diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts index 117894c0d823b5..7e7ea5e6bfdd22 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts @@ -39,7 +39,7 @@ export async function fetchIndexShardSize( ): Promise { const params = { index, - filterPath: ['aggregations.clusters.buckets'], + filter_path: ['aggregations.clusters.buckets'], body: { size: 0, query: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts index cb2f201e2586ec..e57b45e2570fa1 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts @@ -20,7 +20,7 @@ export async function fetchKibanaVersions( ): Promise { const params = { index, - filterPath: ['aggregations'], + filter_path: ['aggregations'], body: { size: 0, query: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts index 5178b6c4c53a71..38ff82cf298329 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -15,7 +15,7 @@ export async function fetchLicenses( ): Promise { const params = { index, - filterPath: [ + filter_path: [ 'hits.hits._source.license.*', 'hits.hits._source.cluster_uuid', 'hits.hits._index', diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts index 6fb54857d40e43..774ee2551ec073 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts @@ -20,7 +20,7 @@ export async function fetchLogstashVersions( ): Promise { const params = { index, - filterPath: ['aggregations'], + filter_path: ['aggregations'], body: { size: 0, query: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts index 46bb9c794a6a69..f34a8dcff1db73 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts @@ -20,7 +20,7 @@ export async function fetchMemoryUsageNodeStats( const clustersIds = clusters.map((cluster) => cluster.clusterUuid); const params = { index, - filterPath: ['aggregations'], + filter_path: ['aggregations'], body: { size: 0, query: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts index a7b4a3a0232072..856ca7c9198851 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts @@ -52,7 +52,7 @@ export async function fetchMissingMonitoringData( const endMs = nowInMs; const params = { index, - filterPath: ['aggregations.clusters.buckets'], + filter_path: ['aggregations.clusters.buckets'], body: { size: 0, query: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts index 5f867ca5b6edf5..dcc8e6516c69bd 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts @@ -30,7 +30,7 @@ export async function fetchNodesFromClusterStats( ): Promise { const params = { index, - filterPath: ['aggregations.clusters.buckets'], + filter_path: ['aggregations.clusters.buckets'], body: { size: 0, sort: [ diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts index 954ec3877144fe..132f7692a7579a 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts @@ -41,7 +41,7 @@ export async function fetchThreadPoolRejectionStats( const clustersIds = clusters.map((cluster) => cluster.clusterUuid); const params = { index, - filterPath: ['aggregations'], + filter_path: ['aggregations'], body: { size: 0, query: { diff --git a/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.ts b/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.ts index 93ff966b5def54..398428f89a4baa 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.ts +++ b/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.ts @@ -30,7 +30,7 @@ export async function getTimeOfLastEvent({ const params = { index: apmIndexPattern, size: 1, - ignoreUnavailable: true, + ignore_unavailable: true, body: { _source: ['beats_stats.timestamp', '@timestamp'], sort: [ diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.ts b/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.ts index 53a4aeb06bcc16..3721bf873a4177 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.ts +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.ts @@ -97,8 +97,8 @@ export async function getApmInfo( const params = { index: apmIndexPattern, size: 1, - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ 'hits.hits._source.beats_stats.beat.host', 'hits.hits._source.beats_stats.beat.version', 'hits.hits._source.beats_stats.beat.name', diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts b/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts index b606eaf5fe7939..be3bb6fdfd6615 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts @@ -109,8 +109,8 @@ export async function getApms(req: LegacyRequest, apmIndexPattern: string, clust const params = { index: apmIndexPattern, size: config.get('monitoring.ui.max_bucket_size'), // FIXME - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ // only filter path can filter for inner_hits 'hits.hits._source.timestamp', 'hits.hits._source.@timestamp', diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.js b/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.js index 45bbe35ebd59d1..0c96e0e2305850 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.js +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.js @@ -46,8 +46,8 @@ export function getApmsForClusters(req, apmIndexPattern, clusters) { const params = { index: apmIndexPattern, size: 0, - ignoreUnavailable: true, - filterPath: apmAggFilterPath, + ignore_unavailable: true, + filter_path: apmAggFilterPath, body: { query: createApmQuery({ start, diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_stats.js b/x-pack/plugins/monitoring/server/lib/apm/get_stats.js index 7dd1b652254cb8..2abd81e325f5ec 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_stats.js +++ b/x-pack/plugins/monitoring/server/lib/apm/get_stats.js @@ -34,9 +34,9 @@ export async function getStats(req, apmIndexPattern, clusterUuid) { const params = { index: apmIndexPattern, - filterPath: apmAggFilterPath, + filter_path: apmAggFilterPath, size: 0, - ignoreUnavailable: true, + ignore_unavailable: true, body: { query: createApmQuery({ start, diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.ts b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.ts index 13b4f0041c99b7..d67f32e64ba71d 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.ts +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.ts @@ -89,8 +89,8 @@ export async function getBeatSummary( const params = { index: beatsIndexPattern, size: 1, - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ 'hits.hits._source.beats_stats.beat.host', 'hits.hits._source.beat.stats.beat.host', 'hits.hits._source.beats_stats.beat.version', diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts b/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts index 641039331bbcb6..fff2b55cf26163 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts @@ -121,8 +121,8 @@ export async function getBeats(req: LegacyRequest, beatsIndexPattern: string, cl const params = { index: beatsIndexPattern, size: config.get('monitoring.ui.max_bucket_size'), // FIXME - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ // only filter path can filter for inner_hits 'hits.hits._source.beats_stats.beat.uuid', 'hits.hits._source.beat.stats.beat.uuid', diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.js b/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.js index 5ceca7a02d89b0..e7c4771dd601cc 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.js @@ -44,8 +44,8 @@ export function getBeatsForClusters(req, beatsIndexPattern, clusters) { const params = { index: beatsIndexPattern, size: 0, - ignoreUnavailable: true, - filterPath: beatsAggFilterPath, + ignore_unavailable: true, + filter_path: beatsAggFilterPath, body: { query: createBeatsQuery({ start, diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.js b/x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.js index f3f296fc09a389..fb40df115d19ac 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.js @@ -81,8 +81,8 @@ export function getLatestStats(req, beatsIndexPattern, clusterUuid) { const params = { index: beatsIndexPattern, size: 0, - ignoreUnavailable: true, - filterPath: 'aggregations', + ignore_unavailable: true, + filter_path: 'aggregations', body: { query: createBeatsQuery({ clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_stats.js b/x-pack/plugins/monitoring/server/lib/beats/get_stats.js index aa3d3947adccf5..3af51d909697ff 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_stats.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_stats.js @@ -33,9 +33,9 @@ export async function getStats(req, beatsIndexPattern, clusterUuid) { const params = { index: beatsIndexPattern, - filterPath: beatsAggFilterPath, + filter_path: beatsAggFilterPath, size: 0, - ignoreUnavailable: true, + ignore_unavailable: true, body: { query: createBeatsQuery({ start, diff --git a/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts index 42041832cfee5d..820b1bf24c6a13 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts @@ -30,8 +30,8 @@ async function findSupportedBasicLicenseCluster( const kibanaDataResult: ElasticsearchResponse = (await callWithRequest(req, 'search', { index: kbnIndexPattern, size: 1, - ignoreUnavailable: true, - filterPath: ['hits.hits._source.cluster_uuid', 'hits.hits._source.cluster.id'], + ignore_unavailable: true, + filter_path: ['hits.hits._source.cluster_uuid', 'hits.hits._source.cluster.id'], body: { sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: { diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.ts index d1fa0efcbca2de..8ed5578e574a0c 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_license.ts @@ -20,8 +20,8 @@ export function getClusterLicense(req: LegacyRequest, esIndexPattern: string, cl const params = { index: esIndexPattern, size: 1, - ignoreUnavailable: true, - filterPath: 'hits.hits._source.license', + ignore_unavailable: true, + filter_path: ['hits.hits._source.license'], body: { sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.ts index 2cec89b18aecda..d732b43bc203b4 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.ts @@ -66,8 +66,8 @@ export function getClustersState( const params = { index: esIndexPattern, size: clusterUuids.length, - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ 'hits.hits._source.cluster_uuid', 'hits.hits._source.elasticsearch.cluster.id', 'hits.hits._source.cluster_state', diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts index 4067293e7829da..6eb21165d7256b 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts @@ -53,8 +53,8 @@ function fetchClusterStats(req: LegacyRequest, esIndexPattern: string, clusterUu const params = { index: esIndexPattern, size: config.get('monitoring.ui.max_bucket_size'), - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ 'hits.hits._index', 'hits.hits._source.cluster_uuid', 'hits.hits._source.elasticsearch.cluster.id', diff --git a/x-pack/plugins/monitoring/server/lib/details/get_series.js b/x-pack/plugins/monitoring/server/lib/details/get_series.js index ca062ad5599fac..d06ff950449dc0 100644 --- a/x-pack/plugins/monitoring/server/lib/details/get_series.js +++ b/x-pack/plugins/monitoring/server/lib/details/get_series.js @@ -150,7 +150,7 @@ async function fetchSeries( const params = { index: indexPattern, size: 0, - ignoreUnavailable: true, + ignore_unavailable: true, body: { query: createQuery({ start: adjustedMin, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.ts index d0abed2ad8b8da..482cbd3601993c 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.ts @@ -27,7 +27,7 @@ export async function checkCcrEnabled(req: LegacyRequest, esIndexPattern: string const params = { index: esIndexPattern, size: 1, - ignoreUnavailable: true, + ignore_unavailable: true, body: { query: createQuery({ type: 'cluster_stats', @@ -38,7 +38,7 @@ export async function checkCcrEnabled(req: LegacyRequest, esIndexPattern: string }), sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], }, - filterPath: [ + filter_path: [ 'hits.hits._source.stack_stats.xpack.ccr', 'hits.hits._source.elasticsearch.cluster.stats.stack.xpack.ccr', ], diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.ts index b6669342649332..43527f875cf729 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.ts @@ -97,7 +97,7 @@ export async function getLastRecovery(req: LegacyRequest, esIndexPattern: string const legacyParams = { index: esIndexPattern, size: 1, - ignoreUnavailable: true, + ignore_unavailable: true, body: { _source: ['index_recovery.shards'], sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.ts index 8b89416f14e9fa..df44036cd0cd82 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.ts @@ -47,8 +47,8 @@ export function getMlJobs(req: LegacyRequest, esIndexPattern: string) { const params = { index: esIndexPattern, size: maxBucketSize, - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ 'hits.hits._source.job_stats.job_id', 'hits.hits._source.elasticsearch.ml.job.id', 'hits.hits._source.job_stats.state', @@ -95,8 +95,8 @@ export function getMlJobsForCluster( const params = { index: esIndexPattern, size: 0, - ignoreUnavailable: true, - filterPath: 'aggregations.jobs_count.value', + ignore_unavailable: true, + filter_path: 'aggregations.jobs_count.value', body: { query: createQuery({ types: ['ml_job', 'job_stats'], start, end, clusterUuid, metric }), aggs: { diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.ts index ee4988d773974f..8a03027a93a565 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_index_summary.ts @@ -89,7 +89,7 @@ export function getIndexSummary( const params = { index: esIndexPattern, size: 1, - ignoreUnavailable: true, + ignore_unavailable: true, body: { sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.ts index 9a61d6c603b973..0a9993769a6e5c 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.ts @@ -116,8 +116,8 @@ export function buildGetIndicesQuery( return { index: esIndexPattern, size, - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ // only filter path can filter for inner_hits 'hits.hits._source.index_stats.index', 'hits.hits._source.elasticsearch.index.name', diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.ts index c7ca66753c73bf..79c44286717b45 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.ts @@ -124,7 +124,7 @@ export function getNodeSummary( const params = { index: esIndexPattern, size: 1, - ignoreUnavailable: true, + ignore_unavailable: true, body: { sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ type: 'node_stats', start, end, clusterUuid, metric, filters }), diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js index 96b434b945ad46..87781417a07e5f 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js @@ -17,8 +17,8 @@ export async function getNodeIds(req, indexPattern, { clusterUuid }, size) { const params = { index: indexPattern, size: 0, - ignoreUnavailable: true, - filterPath: ['aggregations.composite_data.buckets'], + ignore_unavailable: true, + filter_path: ['aggregations.composite_data.buckets'], body: { query: createQuery({ type: 'node_stats', diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.ts index 2235d3d2f3224d..442a2b1b9b9c9b 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.ts @@ -76,7 +76,7 @@ export async function getNodes( const params = { index: esIndexPattern, size: config.get('monitoring.ui.max_bucket_size'), - ignoreUnavailable: true, + ignore_unavailable: true, body: { query: createQuery({ type: 'node_stats', @@ -110,7 +110,7 @@ export async function getNodes( }, sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], }, - filterPath: [ + filter_path: [ 'hits.hits._source.source_node', 'hits.hits._source.service.address', 'hits.hits._source.elasticsearch.node', diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.ts index d3acf8ccaf4437..87f79ff5b9b44d 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_indices_unassigned_shard_stats.ts @@ -41,7 +41,7 @@ async function getUnassignedShardData( const params = { index: esIndexPattern, size: 0, - ignoreUnavailable: true, + ignore_unavailable: true, body: { sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.ts index 7cda87bf09af83..12ce144ebf5c5a 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_nodes_shard_count.ts @@ -39,7 +39,7 @@ async function getShardCountPerNode( const params = { index: esIndexPattern, size: 0, - ignoreUnavailable: true, + ignore_unavailable: true, body: { sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_allocation.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_allocation.ts index b739b4a6533db2..d39e38eab21e93 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_allocation.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_allocation.ts @@ -103,7 +103,7 @@ export function getShardAllocation( const params = { index: esIndexPattern, size: config.get('monitoring.ui.max_bucket_size'), - ignoreUnavailable: true, + ignore_unavailable: true, body: { query: createQuery({ types: ['shard', 'shards'], clusterUuid, metric, filters }), }, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.ts index 5219a2e9286f5b..e0e6854dba05f2 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.ts @@ -96,7 +96,7 @@ export function getShardStats( const params = { index: esIndexPattern, size: 0, - ignoreUnavailable: true, + ignore_unavailable: true, body: { sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query: createQuery({ diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.js index 1895c36699e50f..e4cee4d4455cab 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.js @@ -53,7 +53,7 @@ async function verifyHasPrivileges(req) { }, ], }, - ignoreUnavailable: true, // we allow 404 incase the user shutdown security in-between the check and now + ignore_unavailable: true, // we allow 404 incase the user shutdown security in-between the check and now }); } catch (err) { if ( diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.js b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.js index a1ece9e302b2a7..1c20634c102204 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.js @@ -34,15 +34,6 @@ export async function checkClusterSettings(req) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); const { cloud } = req.server.newPlatform.setup.plugins; const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); - const response = await callWithRequest(req, 'transport.request', { - method: 'GET', - path: '/_cluster/settings?include_defaults', - filter_path: [ - 'persistent.xpack.monitoring', - 'transient.xpack.monitoring', - 'defaults.xpack.monitoring', - ], - }); - + const response = await callWithRequest(req, 'cluster.getSettings', { include_defaults: true }); return handleResponse(response, isCloudEnabled); } diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts b/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts index 3b0af657947e54..15cc9904dd060c 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts @@ -36,8 +36,8 @@ export function getKibanaInfo( const params = { index: kbnIndexPattern, size: 1, - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ 'hits.hits._source.kibana_stats.kibana', 'hits.hits._source.kibana.kibana', 'hits.hits._source.kibana_stats.os.memory.free_in_bytes', diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.ts b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.ts index 4da4c40b25568e..b89ff7a326d80f 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.ts +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.ts @@ -71,7 +71,7 @@ export async function getKibanas( const params = { index: kbnIndexPattern, size: config.get('monitoring.ui.max_bucket_size'), - ignoreUnavailable: true, + ignore_unavailable: true, body: { query: createQuery({ types: ['kibana_stats', 'stats'], diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.js b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.js index 7d648601229224..141596ffd2f6fb 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.js +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.js @@ -37,7 +37,7 @@ export function getKibanasForClusters(req, kbnIndexPattern, clusters) { const params = { index: kbnIndexPattern, size: 0, - ignoreUnavailable: true, + ignore_unavailable: true, body: { query: createQuery({ types: ['stats', 'kibana_stats'], diff --git a/x-pack/plugins/monitoring/server/lib/logs/get_log_types.ts b/x-pack/plugins/monitoring/server/lib/logs/get_log_types.ts index 82b73c6b87f060..bbb48c43033da3 100644 --- a/x-pack/plugins/monitoring/server/lib/logs/get_log_types.ts +++ b/x-pack/plugins/monitoring/server/lib/logs/get_log_types.ts @@ -84,8 +84,8 @@ export async function getLogTypes( const params = { index: filebeatIndexPattern, size: 0, - filterPath: ['aggregations.levels.buckets', 'aggregations.types.buckets'], - ignoreUnavailable: true, + filter_path: ['aggregations.levels.buckets', 'aggregations.types.buckets'], + ignore_unavailable: true, body: { sort: { '@timestamp': { order: 'desc', unmapped_type: 'long' } }, query: { diff --git a/x-pack/plugins/monitoring/server/lib/logs/get_logs.ts b/x-pack/plugins/monitoring/server/lib/logs/get_logs.ts index 11e8f6550bcb7d..4c21422a5d0cf7 100644 --- a/x-pack/plugins/monitoring/server/lib/logs/get_logs.ts +++ b/x-pack/plugins/monitoring/server/lib/logs/get_logs.ts @@ -100,7 +100,7 @@ export async function getLogs( const params = { index: filebeatIndexPattern, size: Math.min(50, config.get('monitoring.ui.elasticsearch.logFetchCount')), - filterPath: [ + filter_path: [ 'hits.hits._source.message', 'hits.hits._source.log.level', 'hits.hits._source.@timestamp', @@ -109,7 +109,7 @@ export async function getLogs( 'hits.hits._source.elasticsearch.index.name', 'hits.hits._source.elasticsearch.node.name', ], - ignoreUnavailable: true, + ignore_unavailable: true, body: { sort: { '@timestamp': { order: 'desc', unmapped_type: 'long' } }, query: { diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.js b/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.js index 58155e35ad52f9..17f76834b333ad 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.js @@ -48,7 +48,7 @@ export function getLogstashForClusters(req, lsIndexPattern, clusters) { const params = { index: lsIndexPattern, size: 0, - ignoreUnavailable: true, + ignore_unavailable: true, body: { query: createQuery({ types: ['stats', 'logstash_stats'], diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts index 954b78c432374f..d047729a0b3c22 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts @@ -45,8 +45,8 @@ export function getNodeInfo( const params = { index: lsIndexPattern, size: 1, - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ 'hits.hits._source.logstash_stats.events', 'hits.hits._source.logstash.node.stats.events', 'hits.hits._source.logstash_stats.jvm.uptime_in_millis', diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.ts index 9db59fec25530e..42d1b69aee5f37 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_nodes.ts @@ -82,7 +82,7 @@ export async function getNodes( const params = { index: lsIndexPattern, size: config.get('monitoring.ui.max_bucket_size'), // FIXME - ignoreUnavailable: true, + ignore_unavailable: true, body: { query: createQuery({ start, diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.js b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.js index 1521c5d3773d0e..2846a968bfed6d 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.js @@ -27,8 +27,8 @@ export async function getLogstashPipelineIds( const params = { index: logstashIndexPattern, size: 0, - ignoreUnavailable: true, - filterPath: ['aggregations.nest.id.buckets', 'aggregations.nest_mb.id.buckets'], + ignore_unavailable: true, + filter_path: ['aggregations.nest.id.buckets', 'aggregations.nest_mb.id.buckets'], body: { query: createQuery({ start, diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.ts index e261505e2d1b01..61c99c3a069b32 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_state_document.ts @@ -42,7 +42,7 @@ export async function getPipelineStateDocument( const params = { index: logstashIndexPattern, size: 1, - ignoreUnavailable: true, + ignore_unavailable: true, body: { _source: { excludes: 'logstash_state.pipeline.representation.plugins' }, sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_stats_aggregation.js b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_stats_aggregation.js index 2e35a4639fa5a6..4d9d2a720a1627 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_stats_aggregation.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_stats_aggregation.js @@ -106,8 +106,8 @@ function fetchPipelineLatestStats( const params = { index: logstashIndexPattern, size: 0, - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ 'aggregations.pipelines.scoped.vertices.vertex_id.buckets.key', 'aggregations.pipelines.scoped.vertices.vertex_id.buckets.events_in_total', 'aggregations.pipelines.scoped.vertices.vertex_id.buckets.events_out_total', diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.js b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.js index d1121c78407ffc..c52d41a363055f 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_versions.js @@ -82,7 +82,7 @@ function fetchPipelineVersions(...args) { const params = { index: logstashIndexPattern, size: 0, - ignoreUnavailable: true, + ignore_unavailable: true, body: { sort: { timestamp: { order: 'desc', unmapped_type: 'long' } }, query, diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex_stats_aggregation.js b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex_stats_aggregation.js index 81d1f2bf572176..97a8c463a2259f 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex_stats_aggregation.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex_stats_aggregation.js @@ -155,8 +155,8 @@ function fetchPipelineVertexTimeSeriesStats( const params = { index: logstashIndexPattern, size: 0, - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ 'aggregations.timeseries.buckets.key', 'aggregations.timeseries.buckets.pipelines.scoped.vertices.vertex_id.events_in_total', 'aggregations.timeseries.buckets.pipelines.scoped.vertices.vertex_id.events_out_total', diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js index 36a48002005b3d..b9ce355b44b625 100644 --- a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js +++ b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.js @@ -57,8 +57,8 @@ const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid, nod const params = { index: Object.values(indexPatterns), size: 0, - ignoreUnavailable: true, - filterPath: ['aggregations.indices.buckets'], + ignore_unavailable: true, + filter_path: ['aggregations.indices.buckets'], body: { query: { bool: { @@ -206,8 +206,8 @@ async function doesIndexExist(req, index) { index, size: 0, terminate_after: 1, - ignoreUnavailable: true, - filterPath: ['hits.total.value'], + ignore_unavailable: true, + filter_path: ['hits.total.value'], }; const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); const response = await callWithRequest(req, 'search', params); diff --git a/x-pack/plugins/monitoring/server/license_service.ts b/x-pack/plugins/monitoring/server/license_service.ts index fde22aaefa5dd2..ab10193fc93cbd 100644 --- a/x-pack/plugins/monitoring/server/license_service.ts +++ b/x-pack/plugins/monitoring/server/license_service.ts @@ -6,16 +6,17 @@ */ import { Subscription } from 'rxjs'; -import { ILegacyCustomClusterClient } from 'kibana/server'; +import { IClusterClient, ILegacyClusterClient } from 'kibana/server'; import { ILicense, LicenseFeature } from '../../licensing/common/types'; import { LicensingPluginStart } from '../../licensing/server'; import { MonitoringConfig } from './config'; import { Logger } from '../../../../src/core/server'; import { MonitoringLicenseService } from './types'; +import { EndpointTypes, Globals, ClientParams } from './static_globals'; interface SetupDeps { licensing: LicensingPluginStart; - monitoringClient: ILegacyCustomClusterClient; + monitoringClient: IClusterClient; config: MonitoringConfig; log: Logger; } @@ -27,8 +28,15 @@ const defaultLicenseFeature: LicenseFeature = { export class LicenseService { public setup({ licensing, monitoringClient, config, log }: SetupDeps): MonitoringLicenseService { + // TODO: This needs to be changed to an IClusterClient as when the Licensing server + // is upgraded to the new client. + const fakeLegacyClusterClient = { + callAsInternalUser: (endpoint: EndpointTypes, options: ClientParams) => + Globals.app.getLegacyClusterShim(monitoringClient.asInternalUser, endpoint, options), + } as ILegacyClusterClient; + const { refresh, license$ } = licensing.createLicensePoller( - monitoringClient, + fakeLegacyClusterClient, config.licensing.api_polling_frequency.asMilliseconds() ); diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 10724594ce576d..ca8d87bd300a08 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -15,11 +15,12 @@ import { KibanaRequest, KibanaResponseFactory, CoreSetup, - ILegacyCustomClusterClient, + ICustomClusterClient, CoreStart, CustomHttpResponseOptions, ResponseError, Plugin, + SharedGlobalConfig, } from 'kibana/server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { @@ -33,8 +34,6 @@ import { MonitoringConfig, createConfig, configSchema } from './config'; import { requireUIRoutes } from './routes'; import { initBulkUploader } from './kibana_monitoring'; import { initInfraSource } from './lib/logs/init_infra_source'; -import { mbSafeQuery } from './lib/mb_safe_query'; -import { instantiateClient } from './es_client/instantiate_client'; import { registerCollectors } from './kibana_monitoring/collectors'; import { registerMonitoringTelemetryCollection } from './telemetry_collection'; import { LicenseService } from './license_service'; @@ -51,7 +50,8 @@ import { RequestHandlerContextMonitoringPlugin, } from './types'; -import { Globals } from './static_globals'; +import { Globals, EndpointTypes } from './static_globals'; +import { instantiateClient } from './es_client/instantiate_client'; // This is used to test the version of kibana const snapshotRegex = /-snapshot/i; @@ -71,50 +71,68 @@ export class MonitoringPlugin private readonly initializerContext: PluginInitializerContext; private readonly log: Logger; private readonly getLogger: (...scopes: string[]) => Logger; - private cluster = {} as ILegacyCustomClusterClient; + private cluster = {} as ICustomClusterClient; private licenseService = {} as MonitoringLicenseService; private monitoringCore = {} as MonitoringCore; private legacyShimDependencies = {} as LegacyShimDependencies; - private bulkUploader: IBulkUploader | undefined; + private bulkUploader?: IBulkUploader; + + private readonly config: MonitoringConfig; + private readonly legacyConfig: SharedGlobalConfig; + private coreSetup?: CoreSetup; + private setupPlugins?: PluginsSetup; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; this.log = initializerContext.logger.get(LOGGING_TAG); this.getLogger = (...scopes: string[]) => initializerContext.logger.get(LOGGING_TAG, ...scopes); + this.config = createConfig(this.initializerContext.config.get>()); + this.legacyConfig = this.initializerContext.config.legacy.get(); } - setup(core: CoreSetup, plugins: PluginsSetup) { - const config = createConfig(this.initializerContext.config.get>()); - const legacyConfig = this.initializerContext.config.legacy.get(); + setup(coreSetup: CoreSetup, plugins: PluginsSetup) { + this.coreSetup = coreSetup; + this.setupPlugins = plugins; - const router = core.http.createRouter(); - this.legacyShimDependencies = { - router, - instanceUuid: this.initializerContext.env.instanceUuid, - esDataClient: core.elasticsearch.legacy.client, - kibanaStatsCollector: plugins.usageCollection?.getCollectorByType( - KIBANA_STATS_TYPE_MONITORING - ), - }; + const serverInfo = coreSetup.http.getServerInfo(); + const kibanaMonitoringLog = this.getLogger(KIBANA_MONITORING_LOGGING_TAG); + this.bulkUploader = initBulkUploader({ + config: this.config, + log: kibanaMonitoringLog, + opsMetrics$: coreSetup.metrics.getOpsMetrics$(), + statusGetter$: coreSetup.status.overall$, + kibanaStats: { + uuid: this.initializerContext.env.instanceUuid, + name: serverInfo.name, + index: this.legacyConfig.kibana.index, + host: serverInfo.hostname, + locale: i18n.getLocale(), + port: serverInfo.port.toString(), + transport_address: `${serverInfo.hostname}:${serverInfo.port}`, + version: this.initializerContext.env.packageInfo.version, + snapshot: snapshotRegex.test(this.initializerContext.env.packageInfo.version), + }, + }); - // Monitoring creates and maintains a connection to a potentially - // separate ES cluster - create this first - const cluster = (this.cluster = instantiateClient( - config.ui.elasticsearch, - this.log, - core.elasticsearch.legacy.createClient - )); + Globals.init({ + initializerContext: this.initializerContext, + config: this.config!, + getLogger: this.getLogger, + log: this.log, + legacyConfig: this.legacyConfig, + coreSetup: this.coreSetup!, + setupPlugins: this.setupPlugins!, + }); - Globals.init(core, plugins.cloud, cluster, config, this.getLogger); - const serverInfo = core.http.getServerInfo(); const alerts = AlertsFactory.getAll(); for (const alert of alerts) { plugins.alerting?.registerType(alert.getAlertType()); } + const config = createConfig(this.initializerContext.config.get>()); // Register collector objects for stats to show up in the APIs if (plugins.usageCollection) { - core.savedObjects.registerType({ + coreSetup.savedObjects.registerType({ name: SAVED_OBJECT_TELEMETRY, hidden: true, namespaceType: 'agnostic', @@ -127,33 +145,40 @@ export class MonitoringPlugin }, }); - registerCollectors(plugins.usageCollection, config, cluster); + registerCollectors(plugins.usageCollection, config, () => this.cluster); registerMonitoringTelemetryCollection( plugins.usageCollection, - cluster, + () => this.cluster, config.ui.max_bucket_size ); } + if (config.ui.enabled) { + this.registerPluginInUI(plugins); + } - // Always create the bulk uploader - const kibanaMonitoringLog = this.getLogger(KIBANA_MONITORING_LOGGING_TAG); - const bulkUploader = (this.bulkUploader = initBulkUploader({ - config, - log: kibanaMonitoringLog, - opsMetrics$: core.metrics.getOpsMetrics$(), - statusGetter$: core.status.overall$, - kibanaStats: { - uuid: this.initializerContext.env.instanceUuid, - name: serverInfo.name, - index: get(legacyConfig, 'kibana.index'), - host: serverInfo.hostname, - locale: i18n.getLocale(), - port: serverInfo.port.toString(), - transport_address: `${serverInfo.hostname}:${serverInfo.port}`, - version: this.initializerContext.env.packageInfo.version, - snapshot: snapshotRegex.test(this.initializerContext.env.packageInfo.version), - }, - })); + return { + // OSS stats api needs to call this in order to centralize how + // we fetch kibana specific stats + getKibanaStats: () => this.bulkUploader?.getKibanaStats() || {}, + }; + } + + init(cluster: ICustomClusterClient, coreStart: CoreStart) { + const config = createConfig(this.initializerContext.config.get>()); + const legacyConfig = this.initializerContext.config.legacy.get(); + const coreSetup = this.coreSetup!; + const plugins = this.setupPlugins!; + + const router = coreSetup.http.createRouter(); + // const [{ elasticsearch }] = await core.getStartServices(); + this.legacyShimDependencies = { + router, + instanceUuid: this.initializerContext.env.instanceUuid, + esDataClient: coreStart.elasticsearch.client.asInternalUser, + kibanaStatsCollector: plugins.usageCollection?.getCollectorByType( + KIBANA_STATS_TYPE_MONITORING + ), + }; // If the UI is enabled, then we want to register it so it shows up // and start any other UI-related setup tasks @@ -162,12 +187,11 @@ export class MonitoringPlugin this.monitoringCore = this.getLegacyShim( config, legacyConfig, - core.getStartServices as () => Promise<[CoreStart, PluginsStart, {}]>, - this.cluster, + coreSetup.getStartServices as () => Promise<[CoreStart, PluginsStart, {}]>, + cluster, plugins ); - this.registerPluginInUI(plugins); requireUIRoutes(this.monitoringCore, { cluster, router, @@ -177,16 +201,18 @@ export class MonitoringPlugin }); initInfraSource(config, plugins.infra); } - - return { - // OSS stats api needs to call this in order to centralize how - // we fetch kibana specific stats - getKibanaStats: () => bulkUploader.getKibanaStats(), - }; } - async start(core: CoreStart, { licensing }: PluginsStart) { - const config = createConfig(this.initializerContext.config.get>()); + async start(coreStart: CoreStart, { licensing }: PluginsStart) { + const config = this.config!; + this.cluster = instantiateClient( + config.ui.elasticsearch, + this.log, + coreStart.elasticsearch.createClient + ); + + this.init(this.cluster, coreStart); + // Start our license service which will ensure // the appropriate licenses are present this.licenseService = new LicenseService().setup({ @@ -209,7 +235,7 @@ export class MonitoringPlugin const monitoringBulkEnabled = mainMonitoring && mainMonitoring.isAvailable && mainMonitoring.isEnabled; if (monitoringBulkEnabled) { - this.bulkUploader?.start(core.elasticsearch.client.asInternalUser); + this.bulkUploader?.start(coreStart.elasticsearch.client.asInternalUser); } else { this.bulkUploader?.handleNotEnabled(); } @@ -227,7 +253,7 @@ export class MonitoringPlugin } stop() { - if (this.cluster) { + if (this.cluster && this.cluster.close) { this.cluster.close(); } if (this.licenseService && this.licenseService.stop) { @@ -281,7 +307,7 @@ export class MonitoringPlugin config: MonitoringConfig, legacyConfig: any, getCoreServices: () => Promise<[CoreStart, PluginsStart, {}]>, - cluster: ILegacyCustomClusterClient, + cluster: ICustomClusterClient, setupPlugins: PluginsSetup ): MonitoringCore { const router = this.legacyShimDependencies.router; @@ -354,12 +380,12 @@ export class MonitoringPlugin }, elasticsearch: { getCluster: (name: string) => ({ - callWithRequest: async (_req: any, endpoint: string, params: any) => { + callWithRequest: async (_req: any, endpoint: EndpointTypes, params: any) => { const client = - name === 'monitoring' ? cluster : this.legacyShimDependencies.esDataClient; - return mbSafeQuery(() => - client.asScoped(req).callAsCurrentUser(endpoint, params) - ); + name === 'monitoring' + ? cluster.asScoped(req).asCurrentUser + : context.core.elasticsearch.client.asCurrentUser; + return await Globals.app.getLegacyClusterShim(client, endpoint, params); }, }), }, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts index e48f424a3d8ee4..e21304e8458e37 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts @@ -93,7 +93,7 @@ export function enableAlertsRoute(server: LegacyServer, npRoute: RouteDependenci let createdAlerts: Array> = []; const disabledWatcherClusterAlerts = await disableWatcherClusterAlerts( - npRoute.cluster.asScoped(request).callAsCurrentUser, + npRoute.cluster.asScoped(request).asCurrentUser, npRoute.logger ); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts index 5cc7046777e265..73b646126ce988 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts @@ -99,7 +99,7 @@ function buildRequest( return { index: esIndexPattern, size: maxBucketSize, - filterPath: [ + filter_path: [ 'hits.hits.inner_hits.by_shard.hits.hits._source.ccr_stats.read_exceptions', 'hits.hits.inner_hits.by_shard.hits.hits._source.elasticsearch.ccr.read_exceptions', 'hits.hits.inner_hits.by_shard.hits.hits._source.ccr_stats.follower_index', diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts index ac5563430fb7ca..5ecb84d97618b0 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts @@ -35,7 +35,7 @@ async function getCcrStat(req: LegacyRequest, esIndexPattern: string, filters: u const params = { index: esIndexPattern, size: 1, - filterPath: [ + filter_path: [ 'hits.hits._source.ccr_stats', 'hits.hits._source.elasticsearch.ccr', 'hits.hits._source.timestamp', diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/cluster.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/cluster.js index b8cdec9ed9b00f..6996c4885d25dc 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/cluster.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/cluster.js @@ -23,6 +23,7 @@ export function clusterSettingsCheckRoute(server) { const response = await checkClusterSettings(req); // needs to be try/catch to handle privilege error return response; } catch (err) { + console.log(err); throw handleSettingsError(err); } }, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts index 1c4ccdada095f6..c2bad7b905c5b1 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts @@ -7,6 +7,7 @@ import { schema } from '@kbn/config-schema'; import { RequestHandlerContext } from 'kibana/server'; +import { estypes } from '@elastic/elasticsearch'; import { INDEX_PATTERN_ELASTICSEARCH, INDEX_PATTERN_KIBANA, @@ -44,11 +45,11 @@ const queryBody = { }; const checkLatestMonitoringIsLegacy = async (context: RequestHandlerContext, index: string) => { - const { client: esClient } = context.core.elasticsearch.legacy; - const result = await esClient.callAsCurrentUser('search', { + const client = context.core.elasticsearch.client.asCurrentUser; + const { body: result } = await client.search>({ index, body: queryBody, - }); + } as estypes.SearchRequest); const { aggregations } = result; const counts = { @@ -62,7 +63,7 @@ const checkLatestMonitoringIsLegacy = async (context: RequestHandlerContext, ind const { types: { buckets }, - } = aggregations; + } = aggregations as { types: { buckets: Array<{ key: string }> } }; counts.mbIndicesCount = buckets.filter(({ key }: { key: string }) => key.includes('-mb-')).length; counts.legacyIndicesCount = buckets.length - counts.mbIndicesCount; diff --git a/x-pack/plugins/monitoring/server/static_globals.ts b/x-pack/plugins/monitoring/server/static_globals.ts index 1bb08d6cafc384..fc4ece32314864 100644 --- a/x-pack/plugins/monitoring/server/static_globals.ts +++ b/x-pack/plugins/monitoring/server/static_globals.ts @@ -5,20 +5,50 @@ * 2.0. */ -import { CoreSetup, ILegacyCustomClusterClient, Logger } from 'kibana/server'; +import { + CoreSetup, + ElasticsearchClient, + Logger, + SharedGlobalConfig, + PluginInitializerContext, +} from 'kibana/server'; import url from 'url'; -import { CloudSetup } from '../../cloud/server'; +import { estypes } from '@elastic/elasticsearch'; import { MonitoringConfig } from './config'; - +import { PluginsSetup } from './types'; +import { mbSafeQuery } from './lib/mb_safe_query'; type GetLogger = (...scopes: string[]) => Logger; +interface InitSetupOptions { + initializerContext: PluginInitializerContext; + coreSetup: CoreSetup; + config: MonitoringConfig; + getLogger: GetLogger; + log: Logger; + legacyConfig: SharedGlobalConfig; + setupPlugins: PluginsSetup; +} + +export type EndpointTypes = + | 'search' + | 'msearch' + | 'transport.request' + | 'cluster.putSettings' + | 'cluster.getSettings' + | string; +export type ClientParams = estypes.SearchRequest | undefined; + interface IAppGlobals { url: string; isCloud: boolean; - monitoringCluster: ILegacyCustomClusterClient; config: MonitoringConfig; getLogger: GetLogger; getKeyStoreValue: (key: string, storeValueMethod?: () => unknown) => unknown; + getLegacyClusterShim: ( + client: ElasticsearchClient, + endpoint: EndpointTypes, + params: ClientParams + ) => any; } interface KeyStoreData { @@ -37,22 +67,35 @@ const getKeyStoreValue = (key: string, storeValueMethod?: () => unknown) => { export class Globals { private static _app: IAppGlobals; - public static init( - coreSetup: CoreSetup, - cloud: CloudSetup | undefined, - monitoringCluster: ILegacyCustomClusterClient, - config: MonitoringConfig, - getLogger: GetLogger - ) { + public static init(options: InitSetupOptions) { + const { coreSetup, setupPlugins, config, getLogger } = options; + const getLegacyClusterShim = async ( + client: ElasticsearchClient, + endpoint: EndpointTypes, + params: ClientParams + ): Promise => + await mbSafeQuery(async () => { + const endpointMap: { [key: string]: (params: any) => any } = { + search: (p) => client.search(p), + msearch: (p) => client.msearch(p), + 'transport.request': (p) => client.transport.request(p), + 'cluster.getSettings': (p) => client.cluster.getSettings(p), + 'cluster.putSettings': (p) => client.cluster.putSettings(p), + }; + const { body } = await endpointMap[endpoint](params); + return body; + }); + const { protocol, hostname, port } = coreSetup.http.getServerInfo(); const pathname = coreSetup.http.basePath.serverBasePath; + Globals._app = { url: url.format({ protocol, hostname, port, pathname }), - isCloud: cloud?.isCloudEnabled || false, - monitoringCluster, + isCloud: setupPlugins.cloud?.isCloudEnabled || false, config, getLogger, getKeyStoreValue, + getLegacyClusterShim, }; } @@ -64,4 +107,6 @@ export class Globals { } return Globals._app; } + + public static stop() {} } diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts index 33487ecafd8c52..8933833bb068a2 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import sinon from 'sinon'; import { getStackStats, getAllStats, handleAllStats } from './get_all_stats'; import { ESClusterStats } from './get_es_stats'; @@ -13,7 +14,11 @@ import { LogstashStatsByClusterUuid } from './get_logstash_stats'; describe('get_all_stats', () => { const timestamp = Date.now(); - const callCluster = sinon.stub(); + const searchMock = sinon.stub(); + const callCluster = ({ search: searchMock } as unknown) as ElasticsearchClient; + afterEach(() => { + searchMock.reset(); + }); const esClusters = [ { cluster_uuid: 'a' }, @@ -157,18 +162,20 @@ describe('get_all_stats', () => { }, ]; - callCluster - .withArgs('search') + searchMock .onCall(0) - .returns(Promise.resolve(esStatsResponse)) + .returns(Promise.resolve({ body: esStatsResponse })) .onCall(1) - .returns(Promise.resolve(kibanaStatsResponse)) + .returns(Promise.resolve({ body: kibanaStatsResponse })) .onCall(2) - .returns(Promise.resolve(logstashStatsResponse)) + .returns(Promise.resolve({ body: logstashStatsResponse })) + .returns(Promise.resolve({ body: logstashStatsResponse })) .onCall(3) - .returns(Promise.resolve({})) // Beats stats + .returns(Promise.resolve({ body: {} })) // Beats stats .onCall(4) - .returns(Promise.resolve({})); // Beats state + .returns(Promise.resolve({ body: {} })) // Beats state + .onCall(5) + .returns(Promise.resolve({ body: {} })); // Logstash state expect(await getAllStats(['a'], callCluster, timestamp, 1)).toStrictEqual(allClusters); }); @@ -178,7 +185,7 @@ describe('get_all_stats', () => { aggregations: { cluster_uuids: { buckets: [] } }, }; - callCluster.withArgs('search').returns(Promise.resolve(clusterUuidsResponse)); + searchMock.returns(Promise.resolve({ body: clusterUuidsResponse })); expect(await getAllStats([], callCluster, timestamp, 1)).toStrictEqual([]); }); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts index 60b107cb293426..a0bad277dacf66 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts @@ -9,7 +9,7 @@ import { set } from '@elastic/safer-lodash-set'; import { get, merge } from 'lodash'; import moment from 'moment'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { LOGSTASH_SYSTEM_ID, KIBANA_SYSTEM_ID, @@ -28,7 +28,7 @@ import { getLogstashStats, LogstashStatsByClusterUuid } from './get_logstash_sta */ export async function getAllStats( clusterUuids: string[], - callCluster: LegacyAPICaller, // TODO: To be changed to the new ES client when the plugin migrates + callCluster: ElasticsearchClient, timestamp: number, maxBucketSize: number ) { diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.test.ts index 3637ace3aeb63e..36477cb48cd891 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.test.ts @@ -7,6 +7,7 @@ import { fetchBeatsStats, processResults } from './get_beats_stats'; import sinon from 'sinon'; +import { ElasticsearchClient } from 'kibana/server'; // eslint-disable-next-line @typescript-eslint/no-var-requires const beatsStatsResultSet = require('./__mocks__/fixtures/beats_stats_results'); @@ -23,40 +24,41 @@ describe('Get Beats Stats', () => { const clusterUuids = ['aCluster', 'bCluster', 'cCluster']; const start = new Date().toISOString(); const end = new Date().toISOString(); - let callCluster = sinon.stub(); + const searchMock = sinon.stub(); + const callCluster = ({ search: searchMock } as unknown) as ElasticsearchClient; beforeEach(() => { const getStub = { get: sinon.stub() }; getStub.get.withArgs('xpack.monitoring.beats.index_pattern').returns('beats-indices-*'); - callCluster = sinon.stub(); + searchMock.reset(); }); it('should set `from: 0, to: 10000` in the query', async () => { + searchMock.returns(Promise.resolve({ body: {} })); await fetchBeatsStats(callCluster, clusterUuids, start, end, {} as any); - const { args } = callCluster.firstCall; - const [api, { body }] = args; + const { args } = searchMock.firstCall; + const [{ body }] = args; - expect(api).toEqual('search'); expect(body.from).toEqual(0); expect(body.size).toEqual(10000); }); it('should set `from: 10000, from: 10000` in the query', async () => { + searchMock.returns(Promise.resolve({ body: {} })); await fetchBeatsStats(callCluster, clusterUuids, start, end, { page: 1 } as any); - const { args } = callCluster.firstCall; - const [api, { body }] = args; + const { args } = searchMock.firstCall; + const [{ body }] = args; - expect(api).toEqual('search'); expect(body.from).toEqual(10000); expect(body.size).toEqual(10000); }); it('should set `from: 20000, from: 10000` in the query', async () => { + searchMock.returns(Promise.resolve({ body: {} })); await fetchBeatsStats(callCluster, clusterUuids, start, end, { page: 2 } as any); - const { args } = callCluster.firstCall; - const [api, { body }] = args; + const { args } = searchMock.firstCall; + const [{ body }] = args; - expect(api).toEqual('search'); expect(body.from).toEqual(20000); expect(body.size).toEqual(10000); }); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts index ba83584e346545..c62fbf4e8a1cfb 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts @@ -7,7 +7,8 @@ import { get } from 'lodash'; import { SearchResponse } from 'elasticsearch'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; +import { estypes } from '@elastic/elasticsearch'; import { createQuery } from './create_query'; import { INDEX_PATTERN_BEATS } from '../../common/constants'; @@ -319,17 +320,17 @@ export function processResults( * @return {Promise} */ async function fetchBeatsByType( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, clusterUuids: string[], start: string, end: string, { page = 0, ...options }: { page?: number } & BeatsProcessOptions, type: string ): Promise { - const params = { + const params: estypes.SearchRequest = { index: INDEX_PATTERN_BEATS, - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ 'hits.hits._source.cluster_uuid', 'hits.hits._source.type', 'hits.hits._source.beats_stats.beat.version', @@ -353,7 +354,7 @@ async function fetchBeatsByType( }, }, ], - }), + }) as estypes.QueryDslQueryContainer, from: page * HITS_SIZE, collapse: { field: `${type}.beat.uuid` }, sort: [{ [`${type}.timestamp`]: { order: 'desc', unmapped_type: 'long' } }], @@ -361,11 +362,11 @@ async function fetchBeatsByType( }, }; - const results = await callCluster>('search', params); + const { body: results } = await callCluster.search(params); const hitsLength = results?.hits?.hits.length || 0; if (hitsLength > 0) { // further augment the clusters object with more stats - processResults(results, options); + processResults(results as SearchResponse, options); if (hitsLength === HITS_SIZE) { // call recursively @@ -383,7 +384,7 @@ async function fetchBeatsByType( } export async function fetchBeatsStats( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, clusterUuids: string[], start: string, end: string, @@ -393,7 +394,7 @@ export async function fetchBeatsStats( } export async function fetchBeatsStates( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, clusterUuids: string[], start: string, end: string, @@ -411,7 +412,7 @@ export interface BeatsStatsByClusterUuid { * @return {Object} - Beats stats in an object keyed by the cluster UUIDs */ export async function getBeatsStats( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, clusterUuids: string[], start: string, end: string diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts index 07d623867bc1c6..7e7ec23b69daf3 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import sinon from 'sinon'; import { getClusterUuids, @@ -13,40 +14,52 @@ import { } from './get_cluster_uuids'; describe('get_cluster_uuids', () => { - const callCluster = sinon.stub(); + const searchMock = sinon.stub(); + const callCluster = ({ search: searchMock } as unknown) as ElasticsearchClient; + + afterEach(() => { + searchMock.reset(); + }); + const response = { - aggregations: { - cluster_uuids: { - buckets: [{ key: 'abc' }, { key: 'xyz' }, { key: '123' }], + body: { + aggregations: { + cluster_uuids: { + buckets: [{ key: 'abc' }, { key: 'xyz' }, { key: '123' }], + }, }, }, }; - const expectedUuids = response.aggregations.cluster_uuids.buckets.map((bucket) => bucket.key); + + const expectedUuids = response.body.aggregations.cluster_uuids.buckets.map( + (bucket) => bucket.key + ); + const timestamp = Date.now(); describe('getClusterUuids', () => { it('returns cluster UUIDs', async () => { - callCluster.withArgs('search').returns(Promise.resolve(response)); + searchMock.returns(Promise.resolve(response)); expect(await getClusterUuids(callCluster, timestamp, 1)).toStrictEqual(expectedUuids); }); }); describe('fetchClusterUuids', () => { it('searches for clusters', async () => { - callCluster.returns(Promise.resolve(response)); - expect(await fetchClusterUuids(callCluster, timestamp, 1)).toStrictEqual(response); + searchMock.returns(Promise.resolve(response)); + expect(await fetchClusterUuids(callCluster, timestamp, 1)).toStrictEqual(response.body); }); }); describe('handleClusterUuidsResponse', () => { - // filterPath makes it easy to ignore anything unexpected because it will come back empty + // filter_path makes it easy to ignore anything unexpected because it will come back empty it('handles unexpected response', () => { const clusterUuids = handleClusterUuidsResponse({}); expect(clusterUuids.length).toStrictEqual(0); }); it('handles valid response', () => { - const clusterUuids = handleClusterUuidsResponse(response); + const clusterUuids = handleClusterUuidsResponse(response.body); expect(clusterUuids).toStrictEqual(expectedUuids); }); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.ts index db7a031385b4a7..7cf4ca2b94ce01 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.ts @@ -7,7 +7,8 @@ import { get } from 'lodash'; import moment from 'moment'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; +import { estypes } from '@elastic/elasticsearch'; import { createQuery } from './create_query'; import { INDEX_PATTERN_ELASTICSEARCH, @@ -18,7 +19,7 @@ import { * Get a list of Cluster UUIDs that exist within the specified timespan. */ export async function getClusterUuids( - callCluster: LegacyAPICaller, // TODO: To be changed to the new ES client when the plugin migrates + callCluster: ElasticsearchClient, timestamp: number, maxBucketSize: number ) { @@ -30,7 +31,7 @@ export async function getClusterUuids( * Fetch the aggregated Cluster UUIDs from the monitoring cluster. */ export async function fetchClusterUuids( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, timestamp: number, maxBucketSize: number ) { @@ -38,13 +39,13 @@ export async function fetchClusterUuids( const end = moment(timestamp).toISOString(); - const params = { + const params: estypes.SearchRequest = { index: INDEX_PATTERN_ELASTICSEARCH, size: 0, - ignoreUnavailable: true, - filterPath: 'aggregations.cluster_uuids.buckets.key', + ignore_unavailable: true, + filter_path: 'aggregations.cluster_uuids.buckets.key', body: { - query: createQuery({ type: 'cluster_stats', start, end }), + query: createQuery({ type: 'cluster_stats', start, end }) as estypes.QueryDslQueryContainer, aggs: { cluster_uuids: { terms: { @@ -56,7 +57,8 @@ export async function fetchClusterUuids( }, }; - return await callCluster('search', params); + const { body: response } = await callCluster.search(params); + return response; } /** diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.test.ts index 033b3ebffe5f16..3b5f654be42223 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import sinon from 'sinon'; import { fetchElasticsearchStats, @@ -13,8 +14,9 @@ import { } from './get_es_stats'; describe('get_es_stats', () => { - const callWith = sinon.stub(); - const response = { + const searchMock = sinon.stub(); + const client = ({ search: searchMock } as unknown) as ElasticsearchClient; + const body = { hits: { hits: [ { _id: 'abc', _source: { cluster_uuid: 'abc' } }, @@ -23,15 +25,15 @@ describe('get_es_stats', () => { ], }, }; - const expectedClusters = response.hits.hits.map((hit) => hit._source); + const expectedClusters = body.hits.hits.map((hit) => hit._source); const clusterUuids = expectedClusters.map((cluster) => cluster.cluster_uuid); const maxBucketSize = 1; describe('getElasticsearchStats', () => { it('returns clusters', async () => { - callWith.withArgs('search').returns(Promise.resolve(response)); + searchMock.returns(Promise.resolve({ body })); - expect(await getElasticsearchStats(callWith, clusterUuids, maxBucketSize)).toStrictEqual( + expect(await getElasticsearchStats(client, clusterUuids, maxBucketSize)).toStrictEqual( expectedClusters ); }); @@ -39,16 +41,16 @@ describe('get_es_stats', () => { describe('fetchElasticsearchStats', () => { it('searches for clusters', async () => { - callWith.returns(response); + searchMock.returns({ body }); - expect(await fetchElasticsearchStats(callWith, clusterUuids, maxBucketSize)).toStrictEqual( - response + expect(await fetchElasticsearchStats(client, clusterUuids, maxBucketSize)).toStrictEqual( + body ); }); }); describe('handleElasticsearchStats', () => { - // filterPath makes it easy to ignore anything unexpected because it will come back empty + // filter_path makes it easy to ignore anything unexpected because it will come back empty it('handles unexpected response', () => { const clusters = handleElasticsearchStats({} as any); @@ -56,7 +58,7 @@ describe('get_es_stats', () => { }); it('handles valid response', () => { - const clusters = handleElasticsearchStats(response as any); + const clusters = handleElasticsearchStats(body as any); expect(clusters).toStrictEqual(expectedClusters); }); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.ts index f309e01c961187..217ab70ab0ecaa 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_es_stats.ts @@ -6,7 +6,8 @@ */ import { SearchResponse } from 'elasticsearch'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; +import { estypes } from '@elastic/elasticsearch'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; /** @@ -17,7 +18,7 @@ import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; * @param {Array} clusterUuids The string Cluster UUIDs to fetch details for */ export async function getElasticsearchStats( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, clusterUuids: string[], maxBucketSize: number ) { @@ -34,16 +35,16 @@ export async function getElasticsearchStats( * * Returns the response for the aggregations to fetch details for the product. */ -export function fetchElasticsearchStats( - callCluster: LegacyAPICaller, +export async function fetchElasticsearchStats( + callCluster: ElasticsearchClient, clusterUuids: string[], maxBucketSize: number ) { - const params = { + const params: estypes.SearchRequest = { index: INDEX_PATTERN_ELASTICSEARCH, size: maxBucketSize, - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ 'hits.hits._source.cluster_uuid', 'hits.hits._source.timestamp', 'hits.hits._source.cluster_name', @@ -69,7 +70,8 @@ export function fetchElasticsearchStats( }, }; - return callCluster('search', params); + const { body: response } = await callCluster.search(params); + return response as SearchResponse; } export interface ESClusterStats { diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.test.ts index e2eed7381f4511..00a14f7b12f7ed 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import sinon from 'sinon'; import { fetchHighLevelStats, @@ -13,12 +14,13 @@ import { } from './get_high_level_stats'; describe('get_high_level_stats', () => { - const callWith = sinon.stub(); + const searchMock = sinon.stub(); + const callCluster = ({ search: searchMock } as unknown) as ElasticsearchClient; const product = 'xyz'; const cloudName = 'bare-metal'; const start = new Date().toISOString(); const end = new Date().toISOString(); - const response = { + const body = { hits: { hits: [ { @@ -232,26 +234,26 @@ describe('get_high_level_stats', () => { describe('getHighLevelStats', () => { it('returns clusters', async () => { - callWith.withArgs('search').returns(Promise.resolve(response)); + searchMock.returns(Promise.resolve({ body })); expect( - await getHighLevelStats(callWith, clusterUuids, start, end, product, maxBucketSize) + await getHighLevelStats(callCluster, clusterUuids, start, end, product, maxBucketSize) ).toStrictEqual(expectedClusters); }); }); describe('fetchHighLevelStats', () => { it('searches for clusters', async () => { - callWith.returns(Promise.resolve(response)); + searchMock.returns(Promise.resolve({ body })); expect( - await fetchHighLevelStats(callWith, clusterUuids, start, end, product, maxBucketSize) - ).toStrictEqual(response); + await fetchHighLevelStats(callCluster, clusterUuids, start, end, product, maxBucketSize) + ).toStrictEqual(body); }); }); describe('handleHighLevelStatsResponse', () => { - // filterPath makes it easy to ignore anything unexpected because it will come back empty + // filter_path makes it easy to ignore anything unexpected because it will come back empty it('handles unexpected response', () => { const clusters = handleHighLevelStatsResponse({} as any, product); @@ -259,7 +261,7 @@ describe('get_high_level_stats', () => { }); it('handles valid response', () => { - const clusters = handleHighLevelStatsResponse(response as any, product); + const clusters = handleHighLevelStatsResponse(body as any, product); expect(clusters).toStrictEqual(expectedClusters); }); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts index 63188be142fdd6..2fb3814ac40224 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts @@ -7,7 +7,8 @@ import { get } from 'lodash'; import { SearchResponse } from 'elasticsearch'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; +import { estypes } from '@elastic/elasticsearch'; import { createQuery } from './create_query'; import { INDEX_PATTERN_KIBANA, @@ -248,7 +249,7 @@ function getIndexPatternForStackProduct(product: string) { * Returns an object keyed by the cluster UUIDs to make grouping easier. */ export async function getHighLevelStats( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, clusterUuids: string[], start: string, end: string, @@ -269,7 +270,7 @@ export async function getHighLevelStats( export async function fetchHighLevelStats< T extends { cluster_uuid?: string } = { cluster_uuid?: string } >( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, clusterUuids: string[], start: string, end: string, @@ -300,14 +301,11 @@ export async function fetchHighLevelStats< filters.push(kibanaFilter); } - const params = { - index: getIndexPatternForStackProduct(product), + const params: estypes.SearchRequest = { + index: getIndexPatternForStackProduct(product) as string, size: maxBucketSize, - headers: { - 'X-QUERY-SOURCE': TELEMETRY_QUERY_SOURCE, - }, - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ 'hits.hits._source.cluster_uuid', `hits.hits._source.${product}_stats.${product}.version`, `hits.hits._source.${product}_stats.os`, @@ -325,7 +323,7 @@ export async function fetchHighLevelStats< end, type: `${product}_stats`, filters, - }), + }) as estypes.QueryDslQueryContainer, collapse: { // a more ideal field would be the concatenation of the uuid + transport address for duped UUIDs (copied installations) field: `${product}_stats.${product}.uuid`, @@ -334,7 +332,12 @@ export async function fetchHighLevelStats< }, }; - return callCluster('search', params); + const { body: response } = await callCluster.search(params, { + headers: { + 'X-QUERY-SOURCE': TELEMETRY_QUERY_SOURCE, + }, + }); + return response as SearchResponse; } /** diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_kibana_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_kibana_stats.ts index 59a77bd7a6b4c0..4be5e02dff3a66 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_kibana_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_kibana_stats.ts @@ -8,7 +8,7 @@ import moment from 'moment'; import { isEmpty } from 'lodash'; import { SearchResponse } from 'elasticsearch'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { KIBANA_SYSTEM_ID, TELEMETRY_COLLECTION_INTERVAL } from '../../common/constants'; import { fetchHighLevelStats, @@ -183,7 +183,7 @@ export function ensureTimeSpan( * specialized usage data that comes with kibana stats (kibana_stats.usage). */ export async function getKibanaStats( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, clusterUuids: string[], start: string, end: string, diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.test.ts index ce4bf523b3e1ae..98f1daf57a85a6 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.test.ts @@ -5,12 +5,14 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import sinon from 'sinon'; import { getLicenses, handleLicenses, fetchLicenses } from './get_licenses'; describe('get_licenses', () => { - const callWith = sinon.stub(); - const response = { + const searchMock = sinon.stub(); + const client = ({ search: searchMock } as unknown) as ElasticsearchClient; + const body = { hits: { hits: [ { _id: 'abc', _source: { cluster_uuid: 'abc', license: { type: 'basic' } } }, @@ -19,7 +21,7 @@ describe('get_licenses', () => { ], }, }; - const expectedClusters = response.hits.hits.map((hit) => hit._source); + const expectedClusters = body.hits.hits.map((hit) => hit._source); const clusterUuids = expectedClusters.map((cluster) => cluster.cluster_uuid); const expectedLicenses = { abc: { type: 'basic' }, @@ -29,22 +31,22 @@ describe('get_licenses', () => { describe('getLicenses', () => { it('returns clusters', async () => { - callWith.withArgs('search').returns(Promise.resolve(response)); + searchMock.returns(Promise.resolve({ body })); - expect(await getLicenses(clusterUuids, callWith, 1)).toStrictEqual(expectedLicenses); + expect(await getLicenses(clusterUuids, client, 1)).toStrictEqual(expectedLicenses); }); }); describe('fetchLicenses', () => { it('searches for clusters', async () => { - callWith.returns(response); + searchMock.returns({ body }); - expect(await fetchLicenses(callWith, clusterUuids, 1)).toStrictEqual(response); + expect(await fetchLicenses(client, clusterUuids, 1)).toStrictEqual(body); }); }); describe('handleLicenses', () => { - // filterPath makes it easy to ignore anything unexpected because it will come back empty + // filter_path makes it easy to ignore anything unexpected because it will come back empty it('handles unexpected response', () => { const clusters = handleLicenses({} as any); @@ -52,7 +54,7 @@ describe('get_licenses', () => { }); it('handles valid response', () => { - const clusters = handleLicenses(response as any); + const clusters = handleLicenses(body as any); expect(clusters).toStrictEqual(expectedLicenses); }); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts index 3115e2adbdad27..633e37a2049bdd 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts @@ -6,7 +6,8 @@ */ import { SearchResponse } from 'elasticsearch'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; +import { estypes } from '@elastic/elasticsearch'; import { ESLicense } from '../../../telemetry_collection_xpack/server'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; @@ -15,7 +16,7 @@ import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; */ export async function getLicenses( clusterUuids: string[], - callCluster: LegacyAPICaller, // TODO: To be changed to the new ES client when the plugin migrates + callCluster: ElasticsearchClient, // TODO: To be changed to the new ES client when the plugin migrates maxBucketSize: number ): Promise<{ [clusterUuid: string]: ESLicense | undefined }> { const response = await fetchLicenses(callCluster, clusterUuids, maxBucketSize); @@ -31,16 +32,16 @@ export async function getLicenses( * * Returns the response for the aggregations to fetch details for the product. */ -export function fetchLicenses( - callCluster: LegacyAPICaller, +export async function fetchLicenses( + callCluster: ElasticsearchClient, clusterUuids: string[], maxBucketSize: number ) { - const params = { + const params: estypes.SearchRequest = { index: INDEX_PATTERN_ELASTICSEARCH, size: maxBucketSize, - ignoreUnavailable: true, - filterPath: ['hits.hits._source.cluster_uuid', 'hits.hits._source.license'], + ignore_unavailable: true, + filter_path: ['hits.hits._source.cluster_uuid', 'hits.hits._source.license'], body: { query: { bool: { @@ -59,7 +60,8 @@ export function fetchLicenses( }, }; - return callCluster('search', params); + const { body: response } = await callCluster.search(params); + return response as SearchResponse; } export interface ESClusterStatsWithLicense { diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_logstash_stats.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_logstash_stats.test.ts index cf1574f8d3f0ee..cd77d28f6c430d 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_logstash_stats.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_logstash_stats.test.ts @@ -12,6 +12,7 @@ import { processLogstashStateResults, } from './get_logstash_stats'; import sinon from 'sinon'; +import { ElasticsearchClient } from 'kibana/server'; // eslint-disable-next-line @typescript-eslint/no-var-requires const logstashStatsResultSet = require('./__mocks__/fixtures/logstash_stats_results'); @@ -33,10 +34,15 @@ const getBaseOptions = () => ({ describe('Get Logstash Stats', () => { const clusterUuids = ['aCluster', 'bCluster', 'cCluster']; - let callCluster = sinon.stub(); + const searchMock = sinon.stub(); + const callCluster = ({ search: searchMock } as unknown) as ElasticsearchClient; beforeEach(() => { - callCluster = sinon.stub(); + searchMock.returns(Promise.resolve({ body: {} })); + }); + + afterEach(() => { + searchMock.reset(); }); describe('fetchLogstashState', () => { @@ -63,17 +69,15 @@ describe('Get Logstash Stats', () => { }; await fetchLogstashState(callCluster, clusterUuid, ephemeralIds, {} as any); - const { args } = callCluster.firstCall; - const [api, { body }] = args; - expect(api).toEqual('search'); + const { args } = searchMock.firstCall; + const [{ body }] = args; expect(body.query).toEqual(expected); }); it('should set `from: 0, to: 10000` in the query', async () => { await fetchLogstashState(callCluster, clusterUuid, ephemeralIds, {} as any); - const { args } = callCluster.firstCall; - const [api, { body }] = args; - expect(api).toEqual('search'); + const { args } = searchMock.firstCall; + const [{ body }] = args; expect(body.from).toEqual(0); expect(body.size).toEqual(10000); }); @@ -82,10 +86,9 @@ describe('Get Logstash Stats', () => { await fetchLogstashState(callCluster, clusterUuid, ephemeralIds, { page: 1, } as any); - const { args } = callCluster.firstCall; - const [api, { body }] = args; + const { args } = searchMock.firstCall; + const [{ body }] = args; - expect(api).toEqual('search'); expect(body.from).toEqual(10000); expect(body.size).toEqual(10000); }); @@ -94,10 +97,9 @@ describe('Get Logstash Stats', () => { await fetchLogstashState(callCluster, clusterUuid, ephemeralIds, { page: 2, } as any); - const { args } = callCluster.firstCall; - const [api, { body }] = args; + const { args } = searchMock.firstCall; + const [{ body }] = args; - expect(api).toEqual('search'); expect(body.from).toEqual(20000); expect(body.size).toEqual(10000); }); @@ -106,30 +108,27 @@ describe('Get Logstash Stats', () => { describe('fetchLogstashStats', () => { it('should set `from: 0, to: 10000` in the query', async () => { await fetchLogstashStats(callCluster, clusterUuids, {} as any); - const { args } = callCluster.firstCall; - const [api, { body }] = args; + const { args } = searchMock.firstCall; + const [{ body }] = args; - expect(api).toEqual('search'); expect(body.from).toEqual(0); expect(body.size).toEqual(10000); }); it('should set `from: 10000, to: 10000` in the query', async () => { await fetchLogstashStats(callCluster, clusterUuids, { page: 1 } as any); - const { args } = callCluster.firstCall; - const [api, { body }] = args; + const { args } = searchMock.firstCall; + const [{ body }] = args; - expect(api).toEqual('search'); expect(body.from).toEqual(10000); expect(body.size).toEqual(10000); }); it('should set `from: 20000, to: 10000` in the query', async () => { await fetchLogstashStats(callCluster, clusterUuids, { page: 2 } as any); - const { args } = callCluster.firstCall; - const [api, { body }] = args; + const { args } = searchMock.firstCall; + const [{ body }] = args; - expect(api).toEqual('search'); expect(body.from).toEqual(20000); expect(body.size).toEqual(10000); }); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_logstash_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_logstash_stats.ts index fafb52d6862b55..ac3f5fceb47ade 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_logstash_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_logstash_stats.ts @@ -6,7 +6,8 @@ */ import { SearchResponse } from 'elasticsearch'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; +import { estypes } from '@elastic/elasticsearch'; import { createQuery } from './create_query'; import { mapToList } from './get_high_level_stats'; import { incrementByKey } from './get_high_level_stats'; @@ -261,17 +262,14 @@ export function processLogstashStateResults( } export async function fetchLogstashStats( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, clusterUuids: string[], { page = 0, ...options }: { page?: number } & LogstashProcessOptions ): Promise { - const params = { - headers: { - 'X-QUERY-SOURCE': TELEMETRY_QUERY_SOURCE, - }, + const params: estypes.SearchRequest = { index: INDEX_PATTERN_LOGSTASH, - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ 'hits.hits._source.cluster_uuid', 'hits.hits._source.type', 'hits.hits._source.source_node', @@ -295,7 +293,7 @@ export async function fetchLogstashStats( }, }, ], - }), + }) as estypes.QueryDslQueryContainer, from: page * HITS_SIZE, collapse: { field: 'logstash_stats.logstash.uuid' }, sort: [{ ['logstash_stats.timestamp']: { order: 'desc', unmapped_type: 'long' } }], @@ -303,12 +301,16 @@ export async function fetchLogstashStats( }, }; - const results = await callCluster>('search', params); + const { body: results } = await callCluster.search(params, { + headers: { + 'X-QUERY-SOURCE': TELEMETRY_QUERY_SOURCE, + }, + }); const hitsLength = results?.hits?.hits.length || 0; if (hitsLength > 0) { // further augment the clusters object with more stats - processStatsResults(results, options); + processStatsResults(results as SearchResponse, options); if (hitsLength === HITS_SIZE) { // call recursively @@ -325,18 +327,15 @@ export async function fetchLogstashStats( } export async function fetchLogstashState( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, clusterUuid: string, ephemeralIds: string[], { page = 0, ...options }: { page?: number } & LogstashProcessOptions ): Promise { - const params = { - headers: { - 'X-QUERY-SOURCE': TELEMETRY_QUERY_SOURCE, - }, + const params: estypes.SearchRequest = { index: INDEX_PATTERN_LOGSTASH, - ignoreUnavailable: true, - filterPath: [ + ignore_unavailable: true, + filter_path: [ 'hits.hits._source.logstash_state.pipeline.batch_size', 'hits.hits._source.logstash_state.pipeline.workers', 'hits.hits._source.logstash_state.pipeline.representation.graph.vertices.config_name', @@ -355,7 +354,7 @@ export async function fetchLogstashState( }, }, ], - }), + }) as estypes.QueryDslQueryContainer, from: page * HITS_SIZE, collapse: { field: 'logstash_state.pipeline.ephemeral_id' }, sort: [{ ['timestamp']: { order: 'desc', unmapped_type: 'long' } }], @@ -363,11 +362,16 @@ export async function fetchLogstashState( }, }; - const results = await callCluster>('search', params); + const { body: results } = await callCluster.search>(params, { + headers: { + 'X-QUERY-SOURCE': TELEMETRY_QUERY_SOURCE, + }, + }); + const hitsLength = results?.hits?.hits.length || 0; if (hitsLength > 0) { // further augment the clusters object with more stats - processLogstashStateResults(results, clusterUuid, options); + processLogstashStateResults(results as SearchResponse, clusterUuid, options); if (hitsLength === HITS_SIZE) { // call recursively @@ -392,7 +396,7 @@ export interface LogstashStatsByClusterUuid { * @return {Object} - Logstash stats in an object keyed by the cluster UUIDs */ export async function getLogstashStats( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, clusterUuids: string[] ): Promise { const options: LogstashProcessOptions = { diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts b/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts index 23c61393899170..bb5d379f3c80c4 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ILegacyClusterClient } from 'kibana/server'; +import type { IClusterClient } from 'kibana/server'; import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import type { UsageStatsPayload } from '../../../../../src/plugins/telemetry_collection_manager/server'; import type { LogstashBaseStats } from './get_logstash_stats'; @@ -31,7 +31,7 @@ interface MonitoringTelemetryUsage { export function registerMonitoringTelemetryCollection( usageCollection: UsageCollectionSetup, - legacyEsClient: ILegacyClusterClient, + getClient: () => IClusterClient, maxBucketSize: number ) { const monitoringStatsCollector = usageCollection.makeStatsCollector< @@ -137,15 +137,13 @@ export function registerMonitoringTelemetryCollection( }, }, }, - fetch: async ({ kibanaRequest }) => { + fetch: async ({ kibanaRequest, esClient }) => { const timestamp = Date.now(); // Collect the telemetry from the monitoring indices for this moment. // NOTE: Usually, the monitoring indices index stats for each product every 10s (by default). // However, some data may be delayed up-to 24h because monitoring only collects extended Kibana stats in that interval // to avoid overloading of the system when retrieving data from the collectors (that delay is dealt with in the Kibana Stats getter inside the `getAllStats` method). // By 8.x, we expect to stop collecting the Kibana extended stats and keep only the monitoring-related metrics. - const callCluster = kibanaRequest - ? legacyEsClient.asScoped(kibanaRequest).callAsCurrentUser - : legacyEsClient.callAsInternalUser; + const callCluster = kibanaRequest ? esClient : getClient().asInternalUser; const clusterDetails = await getClusterUuids(callCluster, timestamp, maxBucketSize); const [licenses, stats] = await Promise.all([ getLicenses(clusterDetails, callCluster, maxBucketSize), diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 3dcf6862b72325..c4a0687bef497b 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -8,9 +8,8 @@ import { Observable } from 'rxjs'; import type { IRouter, - ILegacyClusterClient, Logger, - ILegacyCustomClusterClient, + ICustomClusterClient, RequestHandlerContext, ElasticsearchClient, } from 'kibana/server'; @@ -71,7 +70,7 @@ export interface MonitoringCoreConfig { } export interface RouteDependencies { - cluster: ILegacyCustomClusterClient; + cluster: ICustomClusterClient; router: IRouter; licenseService: MonitoringLicenseService; encryptedSavedObjects?: EncryptedSavedObjectsPluginSetup; @@ -87,7 +86,7 @@ export interface MonitoringCore { export interface LegacyShimDependencies { router: IRouter; instanceUuid: string; - esDataClient: ILegacyClusterClient; + esDataClient: ElasticsearchClient; kibanaStatsCollector: any; } From 57a91215f3180d8a9d2ce4ae8638461a667c4c01 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 29 Jun 2021 20:13:31 -0400 Subject: [PATCH 015/121] fix error handling for repositories API (#103723) --- .../server/routes/api/repositories.test.ts | 3 +-- .../server/routes/api/repositories.ts | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts index c3389e893407d3..ad180b8db45e04 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts @@ -188,11 +188,10 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { const mockEsResponse = { [name]: { type: '', settings: {} }, }; - const mockEsSnapshotError = jest.fn().mockRejectedValueOnce(new Error('snapshot error')); getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); getRepoFn.mockResolvedValue({ body: mockEsResponse }); - getSnapshotFn.mockResolvedValue({ body: mockEsSnapshotError }); + getSnapshotFn.mockRejectedValueOnce(new Error('snapshot error')); const expectedResponse = { repository: { name, ...mockEsResponse[name] }, diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts index 4898c6e299ad30..a0799f4de8c92f 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts @@ -6,7 +6,10 @@ */ import { TypeOf } from '@kbn/config-schema'; -import type { SnapshotRepositorySettings } from '@elastic/elasticsearch/api/types'; +import type { + SnapshotGetRepositoryResponse, + SnapshotRepositorySettings, +} from '@elastic/elasticsearch/api/types'; import { DEFAULT_REPOSITORY_TYPES, REPOSITORY_PLUGINS_MAP } from '../../../common/constants'; import { Repository, RepositoryType } from '../../../common/types'; @@ -101,7 +104,7 @@ export function registerRepositoriesRoutes({ const managedRepository = await getManagedRepositoryName(clusterClient.asCurrentUser); - let repositoryByName: any; + let repositoryByName: SnapshotGetRepositoryResponse; try { ({ body: repositoryByName } = await clusterClient.asCurrentUser.snapshot.getRepository({ @@ -111,12 +114,18 @@ export function registerRepositoriesRoutes({ return handleEsError({ error: e, response: res }); } - const response = await clusterClient.asCurrentUser.snapshot.get({ - repository: name, - snapshot: '_all', - }); - - const { snapshots: snapshotList } = response.body; + const { + body: { snapshots: snapshotList }, + } = await clusterClient.asCurrentUser.snapshot + .get({ + repository: name, + snapshot: '_all', + }) + .catch((e) => ({ + body: { + snapshots: null, + }, + })); if (repositoryByName[name]) { const { type = '', settings = {} } = repositoryByName[name]; From e7e1e3c1f9163f940950b9c41e2e5a0082e66df3 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 29 Jun 2021 18:14:36 -0600 Subject: [PATCH 016/121] Small follow up to PR comments (#103771) ## Summary Small follow up to: https://github.com/elastic/kibana/pull/102280 Where I address PR concerns around docs. --- .../signals/source_fields_merging/README.md | 81 ++++++++++--------- .../utils/recursive_unboxing_fields.ts | 2 +- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/README.md index eb72fc2b32687c..16672a324f8085 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/README.md +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/README.md @@ -1,33 +1,34 @@ Set of utilities for merging between `_source` and `fields` are within this folder as well as strategies for merging between these two. -See `strategies` for the strategies for merging between `_source` and `fields`. See the `utils` folder for the different utilities +See the `strategies` folder for the strategies for merging between `_source` and `fields`. See the `utils` folder for the different utilities which the strategies utilize for help in building their merged documents. -If we run into problems such as ambiguities, uncertainties, or data type contradictions then we will prefer the value within -"doc.fields" when we can. If "doc.fields" contradicts its self or is too ambiguous, then we assume that -there a problem within "doc.fields" due to a malformed runtime field definition and omit the last seen -contradiction. In some cases we might have to omit the merging of the field altogether and instead utilize -the value from "doc._source" - -Hence, these are labeled as "best effort" since we could run into conditions where we should have taken the value -from "doc.fields" but instead did not and took the value from "doc._source". - -If "doc.fields" does not exist we return "doc._source" untouched as-is. If "doc._source" does not exist but -"doc.fields" does exist then we will do a "best effort" to merge "doc.fields" into a fully functional object as -if it was a "doc._source". But again, if we run into contradictions or ambiguities from the -"doc.fields" we will remove that field or omit one of the contradictions. - -If a "doc.field" is found that does not exist in "doc._source" then we merge that "doc.field" into our -return object. - -If we find that a "field" contradicts the "doc._source" object in which we cannot create a regular +The `strategies/merge_all_fields_with_source` strategy will try to merge as much from `fields` as it can into `_source`. This includes + * Data in only `fields` such as `constant_keyword`, `copy_to`, `field alias`, etc... + * It will try to merge data from `fields` into `_source` and overwrite `_source` when it can, which can include data coercion, or different overridden values from runtime fields, etc... + * It will take non-multifield values such as `host.name` from `fields` instead of `host.name.keyword` and merge that as the truth data even though `_source` might have a different original value that is not preserved correctly in `fields` as its choice of which one to take. + * If we run into problems such as ambiguities, uncertainties, or data type contradictions then we will prefer the value within `fields` if we can, but not in all cases. + * It compares and unboxes arrays where it can. + +The `strategies/merge_missing_fields_with_source` strategy is lighter weight and will only merge from `fields` into `_source` when it these conditions + * The value in `_source` is missing but exists in `fields` such as a `constant_keyword`, `copy_to`, `field alias`, etc... + * The value in `fields` is a primitive value and not a nested field or an object type such as a geo-point. + * If we run into problems such as ambiguities, uncertainties, or data type contradictions, then the `fields` value is skipped altogether. + +Hence, these are labeled as "best effort" since we could run into conditions where we should have taken the value from `fields` but instead did not and took the value from +`_source`. If `fields` does not exist we return `_source` untouched as-is for all strategies. If `_source` does not exist but +`fields` does exist then we will do a "best effort" to merge `fields` into a fully functional object as +if it was a `_source` for `strategies/merge_all_fields_with_source` For `strategies/merge_missing_fields_with_source` we will only merge primitive values. In both +strategies if we run into contradictions or ambiguities from `fields` we will remove that field or omit one of the contradictions. + +In all strategies If we find that a `field` contradicts the `_source` object in which we cannot create a regular JSON such as a keyword trying to override an object or an object trying to override a keyword: ``` "fields": { 'foo': 'value_1', foo.bar': 'value_2' } <-- Foo cannot be both an object and a value ``` -Then you will get an object such as +Then you will get an object such as ``` { "foo": "value_1" } ``` @@ -37,33 +38,33 @@ This happens when we have multiFields since multiFields are represented in field fields tries to add multiple overrides or invalid multiFields. Invalid field names such as ".", "..", ".foo", "foo.", ".foo." will be skipped as those cause errors if -we tried to insert them into Elasticsearch as a new field. +we tried to insert them into Elasticsearch as a new field in all strategies -If we encounter an array within "doc._source" that contains an object with more than 1 value and a "field" -tries to add a new element we will not merge that in as we do not know which array to merge that value into. +For `strategies/merge_all_fields_with_source` if we encounter an array within `_source` that contains +an object with more than 1 value and a "field" tries to add a new element we will not merge that in +as we do not know which array to merge that value into. -If we encounter a flattened array in the fields object which is not a nested fields such as: +For `strategies/merge_all_fields_with_source` if we encounter a flattened array in the fields object which is not a nested fields such as: ``` "fields": { "object_name.first" : [ "foo", "bar" ], "object_name.second" : [ "mars", "bar" ] } ``` -and no "doc._source" with the name "object_name", the assumption is that we these are not related and we construct the object as this: - +and no `_source` with the name `object_name`, the assumption is that we these are not related and we construct the object as this: ``` { "object.name": { "first": ["foo", "bar" }, "second": ["mars", "bar"] } ``` -If we detect a "doc._source with a single flattened array sub objects we will prefer the "fields" flattened +For `strategies/merge_all_fields_with_source` if we detect a `_source` with a single flattened array sub objects we will prefer the `fields` flattened array elements and copy them over as-is, which means we could be subtracting elements, adding elements, or completely changing the items from the array. -If we detect an object within the "doc._source" inside of the array, we will not take anything from the -"fields" flattened array elements even if they exist as it is ambiguous where we would put those elements +For `strategies/merge_all_fields_with_source` if we detect an object within the `_source` inside of the array, we will not take anything from the +`fields` flattened array elements even if they exist as it is ambiguous where we would put those elements within the ""doc._source" as an override. -It is best to feed this both the "doc._source" and "doc.fields" values to get the best chances of merging the document correctly. +It is best to feed these strategies both the `_source` and `fields` values to get the best chances of merging the document correctly. -Using these strategies will get you these value types merged that you would otherwise not get directly on your +Using both of these strategies will get you these value types merged that you would otherwise not get directly on your ``` "doc._source": - constant_keyword field @@ -86,7 +87,7 @@ Ambiguities and issues Existing bugs and ambiguities --- -* We currently filter out the geo data points by looking at "type" on the object and filter it out. We could transform it to be valid input at some point. +* For `strategies/merge_all_fields_with_source` we currently filter out the geo data points by looking at "type" on the object and filter it out. We could transform it to be valid input at some point. Tests --- @@ -130,7 +131,7 @@ f_[{}1] f_[{}1, ...1] ``` -fields arrays can contain the following values: +`fields` arrays can contain the following values: ``` undefined f_[] @@ -203,7 +204,7 @@ undefined | f_[{}2] | {} <-- We have an empty object since we o undefined | f_[{}2, ...2] | {} <-- We have an empty object since we only merge primitives ``` -When source key is either a primitive key or a flattened object key with a primitive value (p_p1 or f_p1), +For the `merge_all_fields_with_source` when source key is either a primitive key or a flattened object key with a primitive value (p_p1 or f_p1), then we overwrite source value with fields value as an unboxed value array if fields value is a single array element (f_[p2] or f[{}2]), otherwise we overwrite source as an array. @@ -221,7 +222,7 @@ f_p1 | f_[{}2] | f_{}2 <-- Unboxed from array f_p1 | f_[{}2, ...2] | f_[{}2, ...2] ``` -For the `merge_missing_fields_with_source` none of these will be merged since the source has values such as +For both strategies none of these will be merged since the source has values such as ``` source | fields | value after merge @@ -237,7 +238,7 @@ f_p1 | f_[{}2] | f_p1 f_p1 | f_[{}2, ...2] | f_p1 ``` -When source key is a primitive key or a flattened object key and the source value is any +For the `merge_all_fields_with_source` when source key is a primitive key or a flattened object key and the source value is any type of array (p_[], p_p[p1], or p_p[p1, ...1]) of primitives then we always copy the fields value as is and keep the source key as it was originally (primitive or flattened) @@ -311,10 +312,10 @@ f_[p1, ...1] | f_[{}2] | f_[p1, ...1] f_[p1, ...1] | f_[{}2, ...2] | f_[p1, ...1] ``` -When source key is a primitive key or flattened key and the source value is an object (p_{}1, f_{}1) or -an array containing objects ([p_{1}], f_{}1, p_[{}1, ...1], f_[{}1, ...1]), we only copy the -field value if we detect that field value is also an object meaning that it is a nested field, -(f_[{}]2 or f[{}2, ...2]). We never allow a field to convert an object back into a value. +For the `merge_all_fields_with_source` when source key is a primitive key or flattened key and +the source value is an object (p_{}1, f_{}1) or an array containing objects ([p_{1}], f_{}1, p_[{}1, ...1], +f_[{}1, ...1]), we only copy the field value if we detect that field value is also an object meaning +that it is a nested field, (f_[{}]2 or f[{}2, ...2]). We never allow a field to convert an object back into a value. We never try to merge field values into the array either since they're flattened in the fields and we will have too many ambiguities and issues between the flattened array values and the source objects. diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/recursive_unboxing_fields.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/recursive_unboxing_fields.ts index 9cd0ebcb5a4277..e9967cda363eac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/recursive_unboxing_fields.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/recursive_unboxing_fields.ts @@ -19,7 +19,7 @@ import { FieldsType } from '../types'; * * @param fieldsValue The fields value that contains the nested field or not. * @param valueInMergedDocument The document to compare against fields value to see if it is also an array or not - * @returns + * @returns The unboxed fields if any */ export const recursiveUnboxingFields = ( fieldsValue: FieldsType | FieldsType[0], From 26407550dae787558a404b6f507c50b9319ac0f9 Mon Sep 17 00:00:00 2001 From: Justin Ibarra Date: Tue, 29 Jun 2021 16:24:35 -0800 Subject: [PATCH 017/121] [Detection Rules] Add 7.14 rules (#103730) --- ..._365_brute_force_user_account_attempt.json | 11 ++- ...65_potential_password_spraying_attack.json | 4 +- ...n_shell_execution_via_apple_scripting.json | 4 +- ..._full_network_packet_capture_detected.json | 71 +++++++++++++++++++ .../exfiltration_rds_snapshot_export.json | 48 +++++++++++++ ...e_service_principal_credentials_added.json | 55 ++++++++++++++ .../impact_rds_group_deletion.json | 55 ++++++++++++++ .../rules/prepackaged_rules/index.ts | 36 ++++++++-- ...file_sharing_activity_to_the_internet.json | 7 +- ...ml_auth_rare_hour_for_a_user_to_logon.json | 29 ++++++++ .../ml_auth_rare_source_ip_for_a_user.json | 29 ++++++++ .../ml_auth_rare_user_logon.json | 29 ++++++++ .../ml_auth_spike_in_failed_logon_events.json | 29 ++++++++ .../ml_auth_spike_in_logon_events.json | 29 ++++++++ ...pike_in_logon_events_from_a_source_ip.json | 29 ++++++++ ...istence_folder_action_scripts_runtime.json | 4 +- .../persistence_rds_group_creation.json | 62 ++++++++++++++++ .../persistence_rds_instance_creation.json | 48 +++++++++++++ ...oute_53_domain_transfer_lock_disabled.json | 65 +++++++++++++++++ ...domain_transferred_to_another_account.json | 64 +++++++++++++++++ 20 files changed, 693 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_full_network_packet_capture_detected.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_rds_snapshot_export.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_azure_service_principal_credentials_added.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_group_deletion.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_rare_hour_for_a_user_to_logon.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_rare_source_ip_for_a_user.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_rare_user_logon.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_spike_in_failed_logon_events.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_spike_in_logon_events.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_spike_in_logon_events_from_a_source_ip.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_group_creation.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_instance_creation.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_route_53_domain_transfer_lock_disabled.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_route_53_domain_transferred_to_another_account.json diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_brute_force_user_account_attempt.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_brute_force_user_account_attempt.json index 532b9bf3b17b3a..6bd3606d3b1f99 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_brute_force_user_account_attempt.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_brute_force_user_account_attempt.json @@ -1,6 +1,8 @@ { "author": [ - "Elastic" + "Elastic", + "Willem D'Haese", + "Austin Songer" ], "description": "Identifies attempts to brute force a Microsoft 365 user account. An adversary may attempt a brute force attack to obtain unauthorized access to user accounts.", "false_positives": [ @@ -15,7 +17,10 @@ "license": "Elastic License v2", "name": "Attempts to Brute Force a Microsoft 365 User Account", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:(Exchange or AzureActiveDirectory) and event.category:authentication and \nevent.action:(\"UserLoginFailed\" or \"PasswordLogonInitialAuthUsingPassword\") and event.outcome:failure\n", + "query": "event.dataset:o365.audit and event.provider:(AzureActiveDirectory or Exchange) and\n event.category:authentication and event.action:(UserLoginFailed or PasswordLogonInitialAuthUsingPassword) and\n not o365.audit.LogonError:(UserAccountNotFound or EntitlementGrantsNotFound or UserStrongAuthEnrollmentRequired or\n UserStrongAuthClientAuthNRequired or InvalidReplyTo) and event.outcome:failure\n", + "references": [ + "https://blueteamblog.com/7-ways-to-monitor-your-office-365-logs-using-siem" + ], "risk_score": 73, "rule_id": "26f68dba-ce29-497b-8e13-b4fde1db5a2d", "severity": "high", @@ -51,5 +56,5 @@ "value": 10 }, "type": "threshold", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_potential_password_spraying_attack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_potential_password_spraying_attack.json index 536f893236deeb..c5a20b643b6d78 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_potential_password_spraying_attack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_potential_password_spraying_attack.json @@ -15,7 +15,7 @@ "license": "Elastic License v2", "name": "Potential Password Spraying of Microsoft 365 User Accounts", "note": "## Config\n\nThe Microsoft 365 Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:o365.audit and event.provider:AzureActiveDirectory and event.category:authentication and event.action:UserLoginFailed and event.outcome:failure\n", + "query": "event.dataset:o365.audit and event.provider:(Exchange or AzureActiveDirectory) and event.category:authentication and \nevent.action:(\"UserLoginFailed\" or \"PasswordLogonInitialAuthUsingPassword\") and event.outcome:failure\n", "risk_score": 73, "rule_id": "3efee4f0-182a-40a8-a835-102c68a4175d", "severity": "high", @@ -51,5 +51,5 @@ "value": 25 }, "type": "threshold", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_shell_execution_via_apple_scripting.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_shell_execution_via_apple_scripting.json index b6a285828141c6..6399e3f2031fdb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_shell_execution_via_apple_scripting.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_shell_execution_via_apple_scripting.json @@ -11,7 +11,7 @@ "language": "eql", "license": "Elastic License v2", "name": "Shell Execution via Apple Scripting", - "query": "sequence by host.id with maxspan=5s\n [process where event.type in (\"start\", \"process_started\", \"info\") and process.name == \"osascript\"] by process.pid\n [process where event.type in (\"start\", \"process_started\") and process.name == \"sh\" and process.args == \"-c\"] by process.ppid\n", + "query": "sequence by host.id with maxspan=5s\n [process where event.type in (\"start\", \"process_started\", \"info\") and process.name == \"osascript\"] by process.pid\n [process where event.type in (\"start\", \"process_started\") and process.name == \"sh\" and process.args == \"-c\"] by process.parent.pid\n", "references": [ "https://developer.apple.com/library/archive/technotes/tn2065/_index.html", "https://objectivebythesea.com/v2/talks/OBTS_v2_Thomas.pdf" @@ -44,5 +44,5 @@ } ], "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_full_network_packet_capture_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_full_network_packet_capture_detected.json new file mode 100644 index 00000000000000..88d7e8f262cb54 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_full_network_packet_capture_detected.json @@ -0,0 +1,71 @@ +{ + "author": [ + "Elastic", + "Austin Songer" + ], + "description": "Identifies potential Traffic Mirroring in an Amazon Elastic Compute Cloud (EC2) instance. Traffic Mirroring is an Amazon VPC feature that you can use to copy network traffic from an elastic network interface. This feature can potentially be abused to exfiltrate sensitive data from unencrypted internal traffic.", + "false_positives": [ + "Traffic Mirroring may be done by a system or network administrator. Verify whether the user identity, user agent, and/or hostname should be making changes in your environment. Traffic Mirroring from unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule." + ], + "from": "now-60m", + "index": [ + "filebeat-*", + "logs-aws*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License v2", + "name": "AWS EC2 Full Network Packet Capture Detected", + "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", + "query": "event.dataset:aws.cloudtrail and event.provider:ec2.amazonaws.com and \nevent.action:(CreateTrafficMirrorFilter or CreateTrafficMirrorFilterRule or CreateTrafficMirrorSession or CreateTrafficMirrorTarget) and \nevent.outcome:success\n", + "references": [ + "https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_TrafficMirrorFilter.html", + "https://github.com/easttimor/aws-incident-response" + ], + "risk_score": 47, + "rule_id": "c1812764-0788-470f-8e74-eb4a14d47573", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", + "SecOps", + "Network Security" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0010", + "name": "Exfiltration", + "reference": "https://attack.mitre.org/tactics/TA0010/" + }, + "technique": [ + { + "id": "T1020", + "name": "Automated Exfiltration", + "reference": "https://attack.mitre.org/techniques/T1020/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0009", + "name": "Collection", + "reference": "https://attack.mitre.org/tactics/TA0009/" + }, + "technique": [ + { + "id": "T1074", + "name": "Data Staged", + "reference": "https://attack.mitre.org/techniques/T1074/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_rds_snapshot_export.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_rds_snapshot_export.json new file mode 100644 index 00000000000000..430d97690b6f4d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_rds_snapshot_export.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the export of an Amazon Relational Database Service (RDS) Aurora database snapshot.", + "false_positives": [ + "Exporting snapshots may be done by a system or network administrator. Verify whether the user identity, user agent, and/or hostname should be making changes in your environment. Snapshot exports from unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule." + ], + "from": "now-60m", + "index": [ + "filebeat-*", + "logs-aws*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License v2", + "name": "AWS RDS Snapshot Export", + "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", + "query": "event.dataset:aws.cloudtrail and event.provider:rds.amazonaws.com and event.action:StartExportTask and event.outcome:success\n", + "references": [ + "https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_StartExportTask.html" + ], + "risk_score": 21, + "rule_id": "119c8877-8613-416d-a98a-96b6664ee73a5", + "severity": "low", + "tags": [ + "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", + "SecOps", + "Asset Visibility" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0010", + "name": "Exfiltration", + "reference": "https://attack.mitre.org/tactics/TA0010/" + }, + "technique": [] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_azure_service_principal_credentials_added.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_azure_service_principal_credentials_added.json new file mode 100644 index 00000000000000..0036a719250c18 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_azure_service_principal_credentials_added.json @@ -0,0 +1,55 @@ +{ + "author": [ + "Elastic", + "Austin Songer" + ], + "description": "Identifies when new Service Principal credentials have been added in Azure. In most organizations, credentials will be added to service principals infrequently. Hijacking an application (by adding a rogue secret or certificate) with granted permissions will allow the attacker to access data that is normally protected by MFA requirements.", + "false_positives": [ + "Service principal credential additions may be done by a system or network administrator. Verify whether the username, hostname, and/or resource name should be making changes in your environment. Credential additions from unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule." + ], + "from": "now-25m", + "index": [ + "filebeat-*", + "logs-azure*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License v2", + "name": "Azure Service Principal Credentials Added", + "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", + "query": "event.dataset:azure.auditlogs and azure.auditlogs.operation_name:\"Add service principal credentials.\" and event.outcome:(success or Success)\n", + "references": [ + "https://www.fireeye.com/content/dam/collateral/en/wp-m-unc2452.pdf" + ], + "risk_score": 47, + "rule_id": "f766ffaf-9568-4909-b734-75d19b35cbf4", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Azure", + "Continuous Monitoring", + "SecOps", + "Identity and Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0040", + "name": "Impact", + "reference": "https://attack.mitre.org/tactics/TA0040/" + }, + "technique": [ + { + "id": "T1496", + "name": "Resource Hijacking", + "reference": "https://attack.mitre.org/techniques/T1496/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_group_deletion.json new file mode 100644 index 00000000000000..bc5d808c10c301 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_group_deletion.json @@ -0,0 +1,55 @@ +{ + "author": [ + "Elastic", + "Austin Songer" + ], + "description": "Identifies the deletion of an Amazon Relational Database Service (RDS) Security Group.", + "false_positives": [ + "A RDS security group deletion may be done by a system or network administrator. Verify whether the user identity, user agent, and/or hostname should be making changes in your environment. Security Group deletions from unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule." + ], + "from": "now-60m", + "index": [ + "filebeat-*", + "logs-aws*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License v2", + "name": "AWS RDS Security Group Deletion", + "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", + "query": "event.dataset:aws.cloudtrail and event.provider:rds.amazonaws.com and event.action:DeleteDBSecurityGroup and event.outcome:success\n", + "references": [ + "https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_DeleteDBSecurityGroup.html" + ], + "risk_score": 21, + "rule_id": "863cdf31-7fd3-41cf-a185-681237ea277b", + "severity": "low", + "tags": [ + "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", + "SecOps", + "Monitoring" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0040", + "name": "Impact", + "reference": "https://attack.mitre.org/tactics/TA0040/" + }, + "technique": [ + { + "id": "T1531", + "name": "Account Access Removal", + "reference": "https://attack.mitre.org/techniques/T1531/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts index dc3ca4ceed4c8d..4a6bea85dc6b5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts @@ -545,10 +545,24 @@ import rule532 from './command_and_control_tunneling_via_earthworm.json'; import rule533 from './lateral_movement_evasion_rdp_shadowing.json'; import rule534 from './threat_intel_module_match.json'; import rule535 from './exfiltration_ec2_vm_export_failure.json'; -import rule536 from './defense_evasion_suspicious_execution_from_mounted_device.json'; -import rule537 from './defense_evasion_unusual_network_connection_via_dllhost.json'; -import rule538 from './defense_evasion_amsienable_key_mod.json'; -import rule539 from './persistence_via_bits_job_notify_command.json'; +import rule536 from './exfiltration_ec2_full_network_packet_capture_detected.json'; +import rule537 from './impact_azure_service_principal_credentials_added.json'; +import rule538 from './persistence_route_53_domain_transfer_lock_disabled.json'; +import rule539 from './persistence_route_53_domain_transferred_to_another_account.json'; +import rule540 from './defense_evasion_suspicious_execution_from_mounted_device.json'; +import rule541 from './defense_evasion_unusual_network_connection_via_dllhost.json'; +import rule542 from './defense_evasion_amsienable_key_mod.json'; +import rule543 from './impact_rds_group_deletion.json'; +import rule544 from './persistence_rds_group_creation.json'; +import rule545 from './exfiltration_rds_snapshot_export.json'; +import rule546 from './persistence_rds_instance_creation.json'; +import rule547 from './ml_auth_rare_hour_for_a_user_to_logon.json'; +import rule548 from './ml_auth_rare_source_ip_for_a_user.json'; +import rule549 from './ml_auth_rare_user_logon.json'; +import rule550 from './ml_auth_spike_in_failed_logon_events.json'; +import rule551 from './ml_auth_spike_in_logon_events.json'; +import rule552 from './ml_auth_spike_in_logon_events_from_a_source_ip.json'; +import rule553 from './persistence_via_bits_job_notify_command.json'; export const rawRules = [ rule1, @@ -1090,4 +1104,18 @@ export const rawRules = [ rule537, rule538, rule539, + rule540, + rule541, + rule542, + rule543, + rule544, + rule545, + rule546, + rule547, + rule548, + rule549, + rule550, + rule551, + rule552, + rule553, ]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json index b119dc0a4f211f..29503e050caa76 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json @@ -12,7 +12,10 @@ "language": "kuery", "license": "Elastic License v2", "name": "SMB (Windows File Sharing) Activity to the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:(139 or 445) or event.dataset:zeek.smb) and\n source.ip:(\n 10.0.0.0/8 or\n 172.16.0.0/12 or\n 192.168.0.0/16\n ) and\n not destination.ip:(\n 10.0.0.0/8 or\n 127.0.0.0/8 or\n 169.254.0.0/16 or\n 172.16.0.0/12 or\n 192.168.0.0/16 or\n 224.0.0.0/4 or\n \"::1\" or\n \"FE80::/10\" or\n \"FF00::/8\"\n )\n", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:(139 or 445) or event.dataset:zeek.smb) and\n source.ip:(\n 10.0.0.0/8 or\n 172.16.0.0/12 or\n 192.168.0.0/16\n ) and\n not destination.ip:(\n 10.0.0.0/8 or\n 127.0.0.0/8 or\n 169.254.0.0/16 or\n 172.16.0.0/12 or\n 192.0.0.0/24 or\n 192.0.0.0/29 or\n 192.0.0.8/32 or\n 192.0.0.9/32 or\n 192.0.0.10/32 or\n 192.0.0.170/32 or\n 192.0.0.171/32 or\n 192.0.2.0/24 or\n 192.31.196.0/24 or\n 192.52.193.0/24 or\n 192.168.0.0/16 or\n 192.88.99.0/24 or\n 224.0.0.0/4 or\n 100.64.0.0/10 or\n 192.175.48.0/24 or\n 198.18.0.0/15 or\n 198.51.100.0/24 or\n 203.0.113.0/24 or\n 240.0.0.0/4 or\n \"::1\" or\n \"FE80::/10\" or\n \"FF00::/8\"\n )\n", + "references": [ + "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml" + ], "risk_score": 73, "rule_id": "c82b2bd8-d701-420c-ba43-f11a155b681a", "severity": "high", @@ -57,5 +60,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 8 + "version": 9 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_rare_hour_for_a_user_to_logon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_rare_hour_for_a_user_to_logon.json new file mode 100644 index 00000000000000..8848ec3b7b8c6d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_rare_hour_for_a_user_to_logon.json @@ -0,0 +1,29 @@ +{ + "anomaly_threshold": 75, + "author": [ + "Elastic" + ], + "description": "A machine learning job detected a user logging in at a time of day that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different time zones. In addition, unauthorized user activity often takes place during non-business hours.", + "false_positives": [ + "Users working late, or logging in from unusual time zones while traveling, may trigger this rule." + ], + "from": "now-30m", + "interval": "15m", + "license": "Elastic License v2", + "machine_learning_job_id": "auth_rare_hour_for_a_user", + "name": "Unusual Hour for a User to Logon", + "references": [ + "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "745b0119-0560-43ba-860a-7235dd8cee8d", + "severity": "low", + "tags": [ + "Elastic", + "Authentication", + "Threat Detection", + "ML" + ], + "type": "machine_learning", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_rare_source_ip_for_a_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_rare_source_ip_for_a_user.json new file mode 100644 index 00000000000000..638e18b85bc01d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_rare_source_ip_for_a_user.json @@ -0,0 +1,29 @@ +{ + "anomaly_threshold": 75, + "author": [ + "Elastic" + ], + "description": "A machine learning job detected a user logging in from an IP address that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different locations. An unusual source IP address for a username could also be due to lateral movement when a compromised account is used to pivot between hosts.", + "false_positives": [ + "Business travelers who roam to new locations may trigger this alert." + ], + "from": "now-30m", + "interval": "15m", + "license": "Elastic License v2", + "machine_learning_job_id": "auth_rare_source_ip_for_a_user", + "name": "Unusual Source IP for a User to Logon from", + "references": [ + "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "d4b73fa0-9d43-465e-b8bf-50230da6718b", + "severity": "low", + "tags": [ + "Elastic", + "Authentication", + "Threat Detection", + "ML" + ], + "type": "machine_learning", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_rare_user_logon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_rare_user_logon.json new file mode 100644 index 00000000000000..f72893a0cf252d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_rare_user_logon.json @@ -0,0 +1,29 @@ +{ + "anomaly_threshold": 75, + "author": [ + "Elastic" + ], + "description": "A machine learning job found an unusual user name in the authentication logs. An unusual user name is one way of detecting credentialed access by means of a new or dormant user account. A user account that is normally inactive, because the user has left the organization, which becomes active, may be due to credentialed access using a compromised account password. Threat actors will sometimes also create new users as a means of persisting in a compromised web application.", + "false_positives": [ + "User accounts that are rarely active, such as an SRE or developer logging into a prod server for troubleshooting, may trigger this alert. Under some conditions, a newly created user account may briefly trigger this alert while the model is learning." + ], + "from": "now-30m", + "interval": "15m", + "license": "Elastic License v2", + "machine_learning_job_id": "auth_rare_user", + "name": "Rare User Logon", + "references": [ + "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "138c5dd5-838b-446e-b1ac-c995c7f8108a", + "severity": "low", + "tags": [ + "Elastic", + "Authentication", + "Threat Detection", + "ML" + ], + "type": "machine_learning", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_spike_in_failed_logon_events.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_spike_in_failed_logon_events.json new file mode 100644 index 00000000000000..39f104da673045 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_spike_in_failed_logon_events.json @@ -0,0 +1,29 @@ +{ + "anomaly_threshold": 75, + "author": [ + "Elastic" + ], + "description": "A machine learning job found an unusually large spike in authentication failure events. This can be due to password spraying, user enumeration or brute force activity and may be a precursor to account takeover or credentialed access.", + "false_positives": [ + "A misconfigured service account can trigger this alert. A password change on ana account used by an email client can trigger this alert. Security test cycles that include brute force or password spraying activities may trigger this alert." + ], + "from": "now-30m", + "interval": "15m", + "license": "Elastic License v2", + "machine_learning_job_id": "auth_high_count_logon_fails", + "name": "Spike in Failed Logon Events", + "references": [ + "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "99dcf974-6587-4f65-9252-d866a3fdfd9c", + "severity": "low", + "tags": [ + "Elastic", + "Authentication", + "Threat Detection", + "ML" + ], + "type": "machine_learning", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_spike_in_logon_events.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_spike_in_logon_events.json new file mode 100644 index 00000000000000..d591cc6e0f56d8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_spike_in_logon_events.json @@ -0,0 +1,29 @@ +{ + "anomaly_threshold": 75, + "author": [ + "Elastic" + ], + "description": "A machine learning job found an unusually large spike in successful authentication events. This can be due to password spraying, user enumeration or brute force activity.", + "false_positives": [ + "Build servers and CI systems can sometimes trigger this alert. Security test cycles that include brute force or password spraying activities may trigger this alert." + ], + "from": "now-30m", + "interval": "15m", + "license": "Elastic License v2", + "machine_learning_job_id": "auth_high_count_logon_events", + "name": "Spike in Logon Events", + "references": [ + "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "d7d5c059-c19a-4a96-8ae3-41496ef3bcf9", + "severity": "low", + "tags": [ + "Elastic", + "Authentication", + "Threat Detection", + "ML" + ], + "type": "machine_learning", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_spike_in_logon_events_from_a_source_ip.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_spike_in_logon_events_from_a_source_ip.json new file mode 100644 index 00000000000000..8e007c96c37fbb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_auth_spike_in_logon_events_from_a_source_ip.json @@ -0,0 +1,29 @@ +{ + "anomaly_threshold": 75, + "author": [ + "Elastic" + ], + "description": "A machine learning job found an unusually large spike in successful authentication events events from a particular source IP address. This can be due to password spraying, user enumeration or brute force activity.", + "false_positives": [ + "Build servers and CI systems can sometimes trigger this alert. Security test cycles that include brute force or password spraying activities may trigger this alert." + ], + "from": "now-30m", + "interval": "15m", + "license": "Elastic License v2", + "machine_learning_job_id": "auth_high_count_logon_events_for_a_source_ip", + "name": "Spike in Logon Events from a Source IP", + "references": [ + "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "e26aed74-c816-40d3-a810-48d6fbd8b2fd", + "severity": "low", + "tags": [ + "Elastic", + "Authentication", + "Threat Detection", + "ML" + ], + "type": "machine_learning", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_folder_action_scripts_runtime.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_folder_action_scripts_runtime.json index 369aec91e8f453..fbf9bcc44ed44b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_folder_action_scripts_runtime.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_folder_action_scripts_runtime.json @@ -11,7 +11,7 @@ "language": "eql", "license": "Elastic License v2", "name": "Persistence via Folder Action Script", - "query": "sequence by host.id with maxspan=5s\n [process where event.type in (\"start\", \"process_started\", \"info\") and process.name == \"com.apple.foundation.UserScriptService\"] by process.pid\n [process where event.type in (\"start\", \"process_started\") and process.name in (\"osascript\", \"sh\")] by process.ppid\n", + "query": "sequence by host.id with maxspan=5s\n [process where event.type in (\"start\", \"process_started\", \"info\") and process.name == \"com.apple.foundation.UserScriptService\"] by process.pid\n [process where event.type in (\"start\", \"process_started\") and process.name in (\"osascript\", \"sh\")] by process.parent.pid\n", "references": [ "https://posts.specterops.io/folder-actions-for-persistence-on-macos-8923f222343d" ], @@ -59,5 +59,5 @@ } ], "type": "eql", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_group_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_group_creation.json new file mode 100644 index 00000000000000..b2b5f06f8792a2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_group_creation.json @@ -0,0 +1,62 @@ +{ + "author": [ + "Elastic", + "Austin Songer" + ], + "description": "Identifies the creation of an Amazon Relational Database Service (RDS) Security Group.", + "false_positives": [ + "A RDS security group may be created by a system or network administrator. Verify whether the user identity, user agent, and/or hostname should be making changes in your environment. Security group creations from unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule." + ], + "from": "now-60m", + "index": [ + "filebeat-*", + "logs-aws*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License v2", + "name": "AWS RDS Security Group Creation", + "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", + "query": "event.dataset:aws.cloudtrail and event.provider:rds.amazonaws.com and event.action:CreateDBSecurityGroup and event.outcome:success\n", + "references": [ + "https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_CreateDBSecurityGroup.html" + ], + "risk_score": 21, + "rule_id": "378f9024-8a0c-46a5-aa08-ce147ac73a4e", + "severity": "low", + "tags": [ + "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", + "SecOps", + "Monitoring" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1136", + "name": "Create Account", + "reference": "https://attack.mitre.org/techniques/T1136/", + "subtechnique": [ + { + "id": "T1136.003", + "name": "Cloud Account", + "reference": "https://attack.mitre.org/techniques/T1136/003/" + } + ] + } + ] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_instance_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_instance_creation.json new file mode 100644 index 00000000000000..aa2c946d3a0013 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_instance_creation.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the creation of an Amazon Relational Database Service (RDS) Aurora database instance.", + "false_positives": [ + "A database instance may be created by a system or network administrator. Verify whether the user identity, user agent, and/or hostname should be making changes in your environment. Instances creations from unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule." + ], + "from": "now-60m", + "index": [ + "filebeat-*", + "logs-aws*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License v2", + "name": "AWS RDS Instance Creation", + "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", + "query": "event.dataset:aws.cloudtrail and event.provider:rds.amazonaws.com and event.action:CreateDBInstance and event.outcome:success\n", + "references": [ + "https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_CreateDBInstance.html" + ], + "risk_score": 21, + "rule_id": "f30f3443-4fbb-4c27-ab89-c3ad49d62315", + "severity": "low", + "tags": [ + "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", + "SecOps", + "Asset Visibility" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_route_53_domain_transfer_lock_disabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_route_53_domain_transfer_lock_disabled.json new file mode 100644 index 00000000000000..61ceebb7615f52 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_route_53_domain_transfer_lock_disabled.json @@ -0,0 +1,65 @@ +{ + "author": [ + "Elastic", + "Austin Songer" + ], + "description": "Identifies when a transfer lock was removed from a Route 53 domain. It is recommended to refrain from performing this action unless intending to transfer the domain to a different registrar.", + "false_positives": [ + "A domain transfer lock may be disabled by a system or network administrator. Verify whether the user identity, user agent, and/or hostname should be making changes in your environment. Activity from unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule." + ], + "from": "now-60m", + "index": [ + "filebeat-*", + "logs-aws*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License v2", + "name": "AWS Route 53 Domain Transfer Lock Disabled", + "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", + "query": "event.dataset:aws.cloudtrail and event.provider:route53.amazonaws.com and event.action:DisableDomainTransferLock and event.outcome:success\n", + "references": [ + "https://docs.aws.amazon.com/Route53/latest/APIReference/API_Operations_Amazon_Route_53.html", + "https://docs.aws.amazon.com/Route53/latest/APIReference/API_domains_DisableDomainTransferLock.html" + ], + "risk_score": 21, + "rule_id": "12051077-0124-4394-9522-8f4f4db1d674", + "severity": "low", + "tags": [ + "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", + "SecOps", + "Asset Visibility" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1098", + "name": "Account Manipulation", + "reference": "https://attack.mitre.org/techniques/T1098/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_route_53_domain_transferred_to_another_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_route_53_domain_transferred_to_another_account.json new file mode 100644 index 00000000000000..8485e1a96e685d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_route_53_domain_transferred_to_another_account.json @@ -0,0 +1,64 @@ +{ + "author": [ + "Elastic", + "Austin Songer" + ], + "description": "Identifies when a request has been made to transfer a Route 53 domain to another AWS account.", + "false_positives": [ + "A domain may be transferred to another AWS account by a system or network administrator. Verify whether the user identity, user agent, and/or hostname should be making changes in your environment. Domain transfers from unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule." + ], + "from": "now-60m", + "index": [ + "filebeat-*", + "logs-aws*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License v2", + "name": "AWS Route 53 Domain Transferred to Another Account", + "note": "## Config\n\nThe AWS Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", + "query": "event.dataset:aws.cloudtrail and event.provider:route53.amazonaws.com and event.action:TransferDomainToAnotherAwsAccount and event.outcome:success\n", + "references": [ + "https://docs.aws.amazon.com/Route53/latest/APIReference/API_Operations_Amazon_Route_53.html" + ], + "risk_score": 21, + "rule_id": "2045567e-b0af-444a-8c0b-0b6e2dae9e13", + "severity": "low", + "tags": [ + "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", + "SecOps", + "Asset Visibility" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1098", + "name": "Account Manipulation", + "reference": "https://attack.mitre.org/techniques/T1098/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [] + } + ], + "timestamp_override": "event.ingested", + "type": "query", + "version": 1 +} From a22f08750bbb09b85beeee67513ff83c5328b350 Mon Sep 17 00:00:00 2001 From: debadair Date: Tue, 29 Jun 2021 17:36:09 -0700 Subject: [PATCH 018/121] [DOCS] Updated xref to ES guide (#103803) --- docs/settings/reporting-settings.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 70f3e272fa5a99..8e870ceec8947b 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -105,7 +105,7 @@ security is enabled, < Date: Tue, 29 Jun 2021 20:42:37 -0400 Subject: [PATCH 019/121] [Monitoring] Fix Cluster Listing view (#103718) * check for number of nodes * remove console Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../monitoring/public/components/cluster/listing/listing.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js index 12cfc4f1328634..06a480cd30f189 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js @@ -132,7 +132,9 @@ const getColumns = ( 'data-test-subj': 'nodesCount', sortable: true, render: (total, cluster) => ( - {numeral(total).format('0,0')} + + {typeof total === 'number' ? numeral(total).format('0,0') : 0} + ), }, { From 37e2d8a6c5fc53ad7d6cdb1d6ba1ca5b061e9ca7 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Tue, 29 Jun 2021 20:43:13 -0400 Subject: [PATCH 020/121] [Security Solution][Hosts] Fix Host Events flyout and remove the Endpoint Host Isolation `Take Action` button (only valid for Alerts) (#103784) * Fix bug in `endpointAlertCheck` to ensure events are not looked at * Fix data/type --- .../public/common/mock/mock_detail_item.ts | 4 +- .../common/utils/endpoint_alert_check.test.ts | 50 +++++++++++++------ .../common/utils/endpoint_alert_check.ts | 17 +++++-- .../side_panel/event_details/index.tsx | 2 +- 4 files changed, 54 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts b/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts index 198ab084ae0b87..3712d389edeb10 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts @@ -9,7 +9,7 @@ import { TimelineEventsDetailsItem } from '../../../common/search_strategy'; export const mockDetailItemDataId = 'Y-6TfmcB0WOhS6qyMv3s'; -export const mockDetailItemData: TimelineEventsDetailsItem[] = [ +export const generateMockDetailItemData = (): TimelineEventsDetailsItem[] => [ { field: '_id', originalValue: 'pEMaMmkBUV60JmNWmWVi', @@ -137,3 +137,5 @@ export const mockDetailItemData: TimelineEventsDetailsItem[] = [ isObjectArray: false, }, ]; + +export const mockDetailItemData: TimelineEventsDetailsItem[] = generateMockDetailItemData(); diff --git a/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.test.ts b/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.test.ts index b085fe67d3814a..e95f5c15d4ecb9 100644 --- a/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.test.ts +++ b/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.test.ts @@ -6,26 +6,48 @@ */ import _ from 'lodash'; -import { mockDetailItemData } from '../mock'; +import { generateMockDetailItemData } from '../mock'; import { endpointAlertCheck } from './endpoint_alert_check'; -describe('utils', () => { - describe('endpointAlertCheck', () => { - it('should return false if detections data does not come from endpoint rule', () => { - expect(endpointAlertCheck({ data: mockDetailItemData })).toBeFalsy(); - }); - it('should return true if detections data comes from an endpoint rule', () => { - _.remove(mockDetailItemData, function (o) { - return o.field === 'agent.type'; - }); - const mockEndpointDetailItemData = _.concat(mockDetailItemData, { +describe('Endpoint Alert Check Utility', () => { + let mockDetailItemData: ReturnType; + + beforeEach(() => { + mockDetailItemData = generateMockDetailItemData(); + + // Remove the filebeat agent type from the mock + _.remove(mockDetailItemData, { field: 'agent.type' }); + + mockDetailItemData.push( + // Must be an Alert + { + field: 'signal.rule.id', + category: 'signal', + originalValue: 'endpoint', + values: ['endpoint'], + isObjectArray: false, + }, + // Must be from an endpoint agent + { field: 'agent.type', originalValue: 'endpoint', values: ['endpoint'], isObjectArray: false, - }); + } + ); + }); + + it('should return true if detections data comes from an endpoint rule', () => { + expect(endpointAlertCheck({ data: mockDetailItemData })).toBe(true); + }); + + it('should return false if it is not an Alert (ex. maybe an event)', () => { + _.remove(mockDetailItemData, { field: 'signal.rule.id' }); + expect(endpointAlertCheck({ data: mockDetailItemData })).toBeFalsy(); + }); - expect(endpointAlertCheck({ data: mockEndpointDetailItemData })).toBeTruthy(); - }); + it('should return false if it is not an endpoint agent', () => { + _.remove(mockDetailItemData, { field: 'agent.type' }); + expect(endpointAlertCheck({ data: mockDetailItemData })).toBeFalsy(); }); }); diff --git a/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.ts b/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.ts index e399cec0f3bbe8..30c6e3fdeb672d 100644 --- a/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.ts +++ b/x-pack/plugins/security_solution/public/common/utils/endpoint_alert_check.ts @@ -5,10 +5,21 @@ * 2.0. */ -import { find } from 'lodash/fp'; -import { TimelineEventsDetailsItem } from '../../../common/search_strategy'; +import { find, some } from 'lodash/fp'; +import { TimelineEventsDetailsItem } from '../../../../timelines/common'; + +/** + * Checks to see if the given set of Timeline event detail items includes data that indicates its + * an endpoint Alert. Note that it will NOT match on Events - only alerts + * @param data + */ +export const endpointAlertCheck = ({ data }: { data: TimelineEventsDetailsItem[] }): boolean => { + const isAlert = some({ category: 'signal', field: 'signal.rule.id' }, data); + + if (!isAlert) { + return false; + } -export const endpointAlertCheck = ({ data }: { data: TimelineEventsDetailsItem[] | null }) => { const findEndpointAlert = find({ field: 'agent.type' }, data)?.values; return findEndpointAlert ? findEndpointAlert[0] === 'endpoint' : false; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index 509c629dc287c8..94ef690a1fdc1a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -95,7 +95,7 @@ const EventDetailsPanelComponent: React.FC = ({ const isAlert = some({ category: 'signal', field: 'signal.rule.id' }, detailsData); const isEndpointAlert = useMemo(() => { - return endpointAlertCheck({ data: detailsData }); + return endpointAlertCheck({ data: detailsData || [] }); }, [detailsData]); const agentId = useMemo(() => { From 7d45fcf8ee984b27d24076efe3edfedee17fb992 Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Tue, 29 Jun 2021 20:50:15 -0400 Subject: [PATCH 021/121] [Page layouts] Some light fixes (#103197) * [Solution Toolbar] Fixing button border on non-text color versions * [Alerts] Removed extra wrappers and use EuiPageHeader * [Logstash] Basic conversion to template * [Reporting] Adding bottomBorder to page header * [ML] Fix display of main navigation tabs * [Stack Management] Fix side nav not updating when going back to landing page * [Tags] Add spacing after page header * [License Management] Full width on file uploader * [Page Template] Fixed `emptyState` default template for pages with side nav * [Infra] Removing some page header displays in empty states * [Enterprise Search] Fix some error layouts * [Index Patterns] Quick fix for empty state * snaps * [Page Template] Remove forced padding when `centeredBody` * small hack for tab padding for ml * scroll ML page to fix test * fix test method type signature Co-authored-by: Dave Snider Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Michail Yasonik --- .../__snapshots__/empty_state.test.tsx.snap | 8 +- .../empty_state/empty_state.tsx | 9 +- .../__snapshots__/page_template.test.tsx.snap | 5 - .../public/page_template/page_template.tsx | 7 +- .../public/components/landing/landing.tsx | 13 +- .../management_app/management_router.tsx | 1 + .../solution_toolbar/items/button.scss | 6 +- .../services/common/test_subjects.ts | 7 +- .../error_connecting/error_connecting.tsx | 10 +- .../error_connecting/error_connecting.tsx | 11 +- .../views/error_state/error_state.tsx | 14 +- .../log_entry_categories/page_content.tsx | 10 +- .../logs/log_entry_rate/page_content.tsx | 10 +- .../upload_license.test.tsx.snap | 795 ++--- .../sections/upload_license/upload_license.js | 28 +- .../pipeline_editor.test.js.snap | 3080 ++++++++--------- .../pipeline_editor/pipeline_editor.js | 382 +- .../components/pipeline_list/pipeline_list.js | 65 +- .../plugins/ml/public/application/_index.scss | 1 - .../components/navigation_menu/_index.scss | 1 - .../navigation_menu/_navigation_menu.scss | 5 - .../components/navigation_menu/main_tabs.scss | 4 + .../components/navigation_menu/main_tabs.tsx | 49 +- .../navigation_menu/navigation_menu.tsx | 13 +- .../public/management/report_listing.tsx | 1 + .../public/management/tag_management_page.tsx | 2 + .../public/application/home.tsx | 135 +- .../components/alert_details.tsx | 388 +-- .../services/ml/data_visualizer_file_based.ts | 6 +- 29 files changed, 2437 insertions(+), 2629 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/components/navigation_menu/_index.scss delete mode 100644 x-pack/plugins/ml/public/application/components/navigation_menu/_navigation_menu.scss create mode 100644 x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.scss diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap index 957c94c80680d9..75b8177d9dac35 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap @@ -120,11 +120,10 @@ exports[`EmptyState should render normally 1`] = `
- + - +
diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx index c05f6a1f193b7a..af49e8c36fe3bd 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx @@ -24,6 +24,7 @@ import { EuiCard, EuiLink, EuiText, + EuiFlexGroup, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { reactRouterNavigate } from '../../../../../../plugins/kibana_react/public'; @@ -143,8 +144,8 @@ export const EmptyState = ({
- - + + - + - +
diff --git a/src/plugins/kibana_react/public/page_template/__snapshots__/page_template.test.tsx.snap b/src/plugins/kibana_react/public/page_template/__snapshots__/page_template.test.tsx.snap index 71d034d32bd1be..9ad2bd73674bd8 100644 --- a/src/plugins/kibana_react/public/page_template/__snapshots__/page_template.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/__snapshots__/page_template.test.tsx.snap @@ -2,7 +2,6 @@ exports[`KibanaPageTemplate render basic template 1`] = ` = ({ ); } + const emptyStateDefaultTemplate = pageSideBar ? 'centeredContent' : 'centeredBody'; + /** * An easy way to create the right content for empty pages */ if (isEmptyState && pageHeader && !children) { - template = template ?? 'centeredBody'; + template = template ?? emptyStateDefaultTemplate; const { iconType, pageTitle, description, rightSideItems } = pageHeader; pageHeader = undefined; children = ( @@ -104,14 +106,13 @@ export const KibanaPageTemplate: FunctionComponent = ({ } else if (isEmptyState && pageHeader && children) { template = template ?? 'centeredContent'; } else if (isEmptyState && !pageHeader) { - template = template ?? 'centeredBody'; + template = template ?? emptyStateDefaultTemplate; } return ( void; setBreadcrumbs: () => void; } -export const ManagementLandingPage = ({ version, setBreadcrumbs }: ManagementLandingPageProps) => { +export const ManagementLandingPage = ({ + version, + setBreadcrumbs, + onAppMounted, +}: ManagementLandingPageProps) => { setBreadcrumbs(); + useEffect(() => { + onAppMounted(''); + }, [onAppMounted]); + return ( )} /> diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss b/src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss index 4fc3651ee9f730..a1e5b4e1417652 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss @@ -6,7 +6,9 @@ border-color: $euiBorderColor !important; // sass-lint:disable-line no-important @include kbnThemeStyle('v8') { - border-width: $euiBorderWidthThin; - border-style: solid; + &[class*='--text'] { + border-width: $euiBorderWidthThin; + border-style: solid; + } } } diff --git a/test/functional/services/common/test_subjects.ts b/test/functional/services/common/test_subjects.ts index ae04fe5d2b9390..3f47c6155f1753 100644 --- a/test/functional/services/common/test_subjects.ts +++ b/test/functional/services/common/test_subjects.ts @@ -312,9 +312,12 @@ export class TestSubjects extends FtrService { return testSubjSelector(selector); } - public async scrollIntoView(selector: string) { + public async scrollIntoView( + selector: string, + offset?: number | { topOffset?: number; bottomOffset?: number } + ) { const element = await this.find(selector); - await element.scrollIntoViewIfNecessary(); + await element.scrollIntoViewIfNecessary(offset); } // isChecked always returns false when run on an euiSwitch, because they use the aria-checked attribute diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx index a53e8a099177c2..84dcb07a074743 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { EuiPage, EuiPageContent } from '@elastic/eui'; +import { KibanaPageTemplate } from '../../../../../../../../src/plugins/kibana_react/public'; import { ErrorStatePrompt } from '../../../shared/error_state'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; @@ -19,11 +19,9 @@ export const ErrorConnecting: React.FC = () => { - - - - - + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx index afee20df106e89..979847b4cf1c6d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx @@ -7,16 +7,15 @@ import React from 'react'; -import { EuiPage, EuiPageContent } from '@elastic/eui'; +import { KibanaPageTemplate } from '../../../../../../../../src/plugins/kibana_react/public'; import { ErrorStatePrompt } from '../../../shared/error_state'; import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; export const ErrorConnecting: React.FC = () => ( - + - - - - + + + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx index 8116d55542820b..f4914413a80fa1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx @@ -9,7 +9,7 @@ import React from 'react'; -import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; +import { KibanaPageTemplate } from '../../../../../../../../src/plugins/kibana_react/public'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; import { ErrorStatePrompt } from '../../../shared/error_state'; @@ -19,16 +19,14 @@ import { ViewContentHeader } from '../../components/shared/view_content_header'; export const ErrorState: React.FC = () => { return ( - + <> - + - - - - - + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx index 462b8b2f9dc3ec..bfdbe035261649 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx @@ -116,9 +116,13 @@ const CategoriesPageTemplate: React.FC = ({ return ( {children} diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index ea60d073c23114..04909b059d8c33 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -158,9 +158,13 @@ const AnomaliesPageTemplate: React.FC = ({ return ( {children} diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index 45e7055f4db2bd..ea973df9aad765 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -703,106 +703,85 @@ exports[`UploadLicense should display a modal when license requires acknowledgem
- + } + onChange={[Function]} > -
-
- + + + +
+
- - } - onChange={[Function]} + - -
-
- - - -
- -
- - Select or drag your license file - -
-
-
-
-
-
+ Select or drag your license file +
- +
-
-
-
+
+ + @@ -1445,106 +1424,85 @@ exports[`UploadLicense should display an error when ES says license is expired 1
- + } + onChange={[Function]} > -
-
- + + + +
+
- - } - onChange={[Function]} + - -
-
- - - -
- -
- - Select or drag your license file - -
-
-
-
-
-
+ Select or drag your license file +
- +
-
-
-
+ + + @@ -2187,106 +2145,85 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 - + } + onChange={[Function]} > -
-
- + + + +
+
- - } - onChange={[Function]} + - -
-
- - - -
- -
- - Select or drag your license file - -
-
-
-
-
-
+ Select or drag your license file +
- +
-
-
-
+ + + @@ -2929,106 +2866,85 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`] - + } + onChange={[Function]} > -
-
- + + + +
+
- - } - onChange={[Function]} + - -
-
- - - -
- -
- - Select or drag your license file - -
-
-
-
-
-
+ Select or drag your license file +
- +
-
-
-
+ + + @@ -3671,106 +3587,85 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` - + } + onChange={[Function]} > -
-
- + + + +
+
- - } - onChange={[Function]} + - -
-
- - - -
- -
- - Select or drag your license file - -
-
-
-
-
-
+ Select or drag your license file +
- +
-
-
-
+ + + diff --git a/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js b/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js index 8ae20a2e607570..22933c6207a725 100644 --- a/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js +++ b/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js @@ -160,22 +160,18 @@ export class UploadLicense extends React.PureComponent { - - - - - } - onChange={this.handleFile} - /> - - - + + } + onChange={this.handleFile} + /> + {shouldShowTelemetryOptIn(telemetry) && ( - + + - -

- Create Pipeline -

-
- - } - isInvalid={true} + labelType="label" > - - } - labelType="label" - > - + + - - + - } - labelType="label" - > - + + - - - } - labelType="label" + } + labelType="label" + > +
-
- -
- - +
+
+ - } - labelType="label" + /> + } + labelType="label" + > + + + + + - - - - - - - - - - - - + + + + - - - + + - - - - - - + + + + + - - - -
- - + + + +
+ + + - - - - - - + + + + - - - - - -
- + + + + + `; exports[`PipelineEditor component invalidates form for invalid pipeline id input 1`] = ` -
- + + - -

- Create Pipeline -

-
- - } - isInvalid={true} + labelType="label" > - - } - labelType="label" - > - + + - - + - } - labelType="label" - > - + + - - - } - labelType="label" + } + labelType="label" + > +
-
- -
- - +
+
+ - } - labelType="label" + /> + } + labelType="label" + > + + + + + - - - - - - - - - - - - + + + + - - - + + - - - - - - + + + + + - - - -
- - + + + +
+ + + - - - - - - + + + + - - - - - -
-
+ + + + + `; exports[`PipelineEditor component invalidates form for pipeline id with spaces 1`] = ` -
- + + - -

- Create Pipeline -

-
- - } - isInvalid={true} + labelType="label" > - - } - labelType="label" - > - + + - - + - } - labelType="label" - > - + + - - - } - labelType="label" + } + labelType="label" + > +
-
- -
- - +
+
+ - } - labelType="label" + /> + } + labelType="label" + > + + + + + - - - - - - - - - - - - + + + + - - - + + - - - - - - + + + + + - - - -
- - + + + +
+ + + - - - - - - + + + + - - - - - -
-
+ + + + + `; exports[`PipelineEditor component matches snapshot for clone pipeline 1`] = ` -
- + + + - -

- Clone Pipeline "pipelineToClone" -

-
- - + } + labelType="label" > - - } - labelType="label" - > - + + - - - } - labelType="label" + } + labelType="label" + > +
-
- -
- - +
+
+ - } - labelType="label" + /> + } + labelType="label" + > + + + + + - - - - - - - - - - - - + + + + - - - + + - - - - - - + + + + + - - - -
- - + + + +
+ + + - - - - - - + + + + - - - - - + + + + - - - - - -
-
+ + + + + `; exports[`PipelineEditor component matches snapshot for create pipeline 1`] = ` -
- + + + - -

- Create Pipeline -

-
- - + } + labelType="label" > - - } - labelType="label" - > - + + - - + - } - labelType="label" - > - + + - - - } - labelType="label" + } + labelType="label" + > +
-
- -
- - +
+
+ - } - labelType="label" + /> + } + labelType="label" + > + + + + + - - - - - - - - - - - - + + + + - - - + + - - - - - - + + + + + - - - -
- - + + + +
+ + + - - - - - - + + + + - - - - - -
-
+ + + + + `; exports[`PipelineEditor component matches snapshot for edit pipeline 1`] = ` -
- + + + - -

- Edit Pipeline "pipelineId" -

-
- - + } + labelType="label" > - - } - labelType="label" - > - + + - - - } - labelType="label" + } + labelType="label" + > +
-
- -
- - +
+
+ - } - labelType="label" + /> + } + labelType="label" + > + + + + + - - - - - - - - - - - - + + + + - - - + + - - - - - - + + + + + - - - -
- - + + + +
+ + + - - - - - - + + + + - - - - - + + + + - - - - - -
-
+ + + + + `; diff --git a/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js index 2ec7caa976cc8b..334c7d0322a849 100644 --- a/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js +++ b/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js @@ -25,10 +25,10 @@ import { EuiFieldText, EuiForm, EuiFormRow, - EuiPageContent, + EuiPageContentBody, EuiSelect, EuiSpacer, - EuiTitle, + EuiPageHeader, } from '@elastic/eui'; import { ConfirmDeletePipelineModal } from './confirm_delete_pipeline_modal'; import { FlexItemSetting } from './flex_item_setting'; @@ -269,219 +269,211 @@ class PipelineEditorUi extends React.Component { const { intl } = this.props; return ( -
- - -

{this.getPipelineHeadingText()}

-
- - - {this.props.isNewPipeline && ( - - } - > - - - )} + + + + + {this.props.isNewPipeline && ( } > - + } + > + - } + name="pipelineDescription" + onChange={this.onPipelineDescriptionChange} + value={this.state.pipeline.description || ''} + /> + + + } + > +
+ +
+
+ + } + > + this.handleNumberChange('pipeline.workers', e.target.value)} + value={this.state.pipeline.settings['pipeline.workers']} + /> + + + + -
- -
- - - } + this.handleNumberChange('pipeline.batch.size', e.target.value)} + value={this.state.pipeline.settings['pipeline.batch.size']} + /> +
+ this.handleNumberChange('pipeline.workers', e.target.value)} - value={this.state.pipeline.settings['pipeline.workers']} + data-test-subj="inputBatchDelay" + onChange={(e) => this.handleNumberChange('pipeline.batch.delay', e.target.value)} + value={this.state.pipeline.settings['pipeline.batch.delay']} /> - - - - - this.handleNumberChange('pipeline.batch.size', e.target.value)} - value={this.state.pipeline.settings['pipeline.batch.size']} - /> - - - this.handleNumberChange('pipeline.batch.delay', e.target.value)} - value={this.state.pipeline.settings['pipeline.batch.delay']} - /> - - - - - this.handleSettingChange('queue.type', e.target.value)} - options={PIPELINE_EDITOR.QUEUE_TYPES} - value={this.state.pipeline.settings['queue.type']} - /> - - - this.handleMaxByteNumberChange(e.target.value)} - value={this.state.maxBytesNumber} - /> - - - this.handleMaxByteUnitChange(e.target.value)} - options={PIPELINE_EDITOR.UNITS} - value={this.state.maxBytesUnit} - /> - - - - this.handleNumberChange('queue.checkpoint.writes', e.target.value) - } - value={this.state.pipeline.settings['queue.checkpoint.writes']} - /> - - -
- - + + + + + this.handleSettingChange('queue.type', e.target.value)} + options={PIPELINE_EDITOR.QUEUE_TYPES} + value={this.state.pipeline.settings['queue.type']} + /> + + + this.handleMaxByteNumberChange(e.target.value)} + value={this.state.maxBytesNumber} + /> + + + this.handleMaxByteUnitChange(e.target.value)} + options={PIPELINE_EDITOR.UNITS} + value={this.state.maxBytesUnit} + /> + + + this.handleNumberChange('queue.checkpoint.writes', e.target.value)} + value={this.state.pipeline.settings['queue.checkpoint.writes']} + /> + + +
+ + + + + + + + + + + + + {!this.props.isNewPipeline && ( - - - - - - - + - {!this.props.isNewPipeline && ( - - - - - - )} - -
+ )} + {this.state.showConfirmDeleteModal && ( )} -
+ ); } } diff --git a/x-pack/plugins/logstash/public/application/components/pipeline_list/pipeline_list.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/pipeline_list.js index a8b03d91f996dd..4772669daec62d 100644 --- a/x-pack/plugins/logstash/public/application/components/pipeline_list/pipeline_list.js +++ b/x-pack/plugins/logstash/public/application/components/pipeline_list/pipeline_list.js @@ -12,9 +12,8 @@ import { EuiCallOut, EuiEmptyPrompt, EuiLoadingSpinner, - EuiPageContent, - EuiTitle, - EuiText, + EuiPageContentBody, + EuiPageHeader, EuiSpacer, } from '@elastic/eui'; @@ -292,36 +291,34 @@ class PipelineListUi extends React.Component { const { clonePipeline, createPipeline, isReadOnly, openPipeline } = this.props; const { isSelectable, message, pipelines, selection, showConfirmDeleteModal } = this.state; return ( -
- - -

- -

-
- -

- -

-
- - {this.renderNoPermissionCallOut()} - -
+ + + } + description={ + + } + bottomBorder + /> + + {this.renderNoPermissionCallOut()} + + -
+ ); } } diff --git a/x-pack/plugins/ml/public/application/_index.scss b/x-pack/plugins/ml/public/application/_index.scss index fb61a2a16c19d5..8fec50ed0e0196 100644 --- a/x-pack/plugins/ml/public/application/_index.scss +++ b/x-pack/plugins/ml/public/application/_index.scss @@ -25,7 +25,6 @@ @import 'components/items_grid/index'; @import 'components/job_selector/index'; @import 'components/loading_indicator/index'; // SASSTODO: This component should be replaced with EuiLoadingSpinner - @import 'components/navigation_menu/index'; @import 'components/rule_editor/index'; // SASSTODO: This file overwrites EUI directly @import 'components/stats_bar/index'; @import 'components/ml_embedded_map/index'; diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/_index.scss b/x-pack/plugins/ml/public/application/components/navigation_menu/_index.scss deleted file mode 100644 index 5135bba535dd9b..00000000000000 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'navigation_menu' diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/_navigation_menu.scss b/x-pack/plugins/ml/public/application/components/navigation_menu/_navigation_menu.scss deleted file mode 100644 index 0d14bb46e8deb8..00000000000000 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/_navigation_menu.scss +++ /dev/null @@ -1,5 +0,0 @@ -.mlNavigationMenu { - padding: 0 $euiSizeM; - border-bottom: $euiBorderThin; - background-color: $euiColorEmptyShade; -} diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.scss b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.scss new file mode 100644 index 00000000000000..05b85e4d20633a --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.scss @@ -0,0 +1,4 @@ +.mlMainTabs { + // Hack to address https://github.com/elastic/kibana/pull/103197#discussion_r659645946 + padding-bottom: 0 !important; +} diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx index 0f381fb7acee91..5073896eba9f80 100644 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx +++ b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx @@ -7,13 +7,14 @@ import React, { FC, useState, useEffect } from 'react'; -import { EuiTabs, EuiTab } from '@elastic/eui'; +import { EuiPageHeader } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TabId } from './navigation_menu'; import { useMlKibana, useMlUrlGenerator, useNavigateToPath } from '../../contexts/kibana'; import { MlUrlGeneratorState } from '../../../../common/types/ml_url_generator'; import { useUrlState } from '../../util/url_state'; import { ML_APP_NAME } from '../../../../common/constants/app'; +import './main_tabs.scss'; export interface Tab { id: TabId; @@ -154,40 +155,26 @@ export const MainTabs: FC = ({ tabId, disableLinks }) => { }, [selectedTabId]); return ( - - {tabs.map((tab: Tab) => { + { const { id, disabled } = tab; const testSubject = TAB_DATA[id].testSubject; const defaultPathId = (TAB_DATA[id].pathId || id) as MlUrlGeneratorState['page']; - return disabled ? ( -
- - {tab.name} - -
- ) : ( -
- { - onSelectedTabChanged(id); - redirectToTab(defaultPathId); - }} - isSelected={id === selectedTabId} - key={`tab-${id}-key`} - > - {tab.name} - -
- ); + return { + label: tab.name, + disabled, + onClick: () => { + onSelectedTabChanged(id); + redirectToTab(defaultPathId); + }, + 'data-test-subj': testSubject + (id === selectedTabId ? ' selected' : ''), + isSelected: id === selectedTabId, + }; })} -
+ /> ); }; diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/navigation_menu.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/navigation_menu.tsx index e836dc3ddced72..986a88d789b369 100644 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/navigation_menu.tsx +++ b/x-pack/plugins/ml/public/application/components/navigation_menu/navigation_menu.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import React, { Fragment, FC } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { FC } from 'react'; import { isFullLicense } from '../../license'; @@ -27,13 +26,5 @@ interface Props { export const NavigationMenu: FC = ({ tabId }) => { const disableLinks = isFullLicense() === false; - return ( - - - - - - - - ); + return ; }; diff --git a/x-pack/plugins/reporting/public/management/report_listing.tsx b/x-pack/plugins/reporting/public/management/report_listing.tsx index fffa952be6cb43..0b6ece4d8bd021 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.tsx @@ -135,6 +135,7 @@ class ReportListingUi extends Component { return ( <> } diff --git a/x-pack/plugins/saved_objects_tagging/public/management/tag_management_page.tsx b/x-pack/plugins/saved_objects_tagging/public/management/tag_management_page.tsx index e671bc587cf1f9..e36d1a8afc08f3 100644 --- a/x-pack/plugins/saved_objects_tagging/public/management/tag_management_page.tsx +++ b/x-pack/plugins/saved_objects_tagging/public/management/tag_management_page.tsx @@ -11,6 +11,7 @@ import useMount from 'react-use/lib/useMount'; import { Query } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ChromeBreadcrumb, CoreStart } from 'src/core/public'; +import { EuiSpacer } from '@elastic/eui'; import { TagWithRelations, TagsCapabilities } from '../../common'; import { getCreateModalOpener } from '../components/edition_modal'; import { ITagInternalClient, ITagAssignmentService, ITagsCache } from '../services'; @@ -194,6 +195,7 @@ export const TagManagementPage: FC = ({ return ( <>
+ - - - - -

- -

-
- - - - - -
-
- - -

+ <> + -

-
- - - {tabs.map((tab) => ( - onSectionChange(tab.id)} - isSelected={tab.id === section} - key={tab.id} - data-test-subj={`${tab.id}Tab`} - > - {tab.name} - - ))} - - - - - - - - {canShowActions && ( - - )} + + } + rightSideItems={[ + + + , + ]} + description={ + + } + tabs={tabs.map((tab) => ({ + label: tab.name, + onClick: () => onSectionChange(tab.id), + isSelected: tab.id === section, + key: tab.id, + 'data-test-subj': `${tab.id}Tab`, + }))} + /> + + + + + + + {canShowActions && ( - - - -
- + )} + + + + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 3e411913520adc..02aa5f9b0b828f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -10,8 +10,6 @@ import React, { useState, useEffect, useReducer } from 'react'; import { keyBy } from 'lodash'; import { useHistory } from 'react-router-dom'; import { - EuiPageBody, - EuiPageContent, EuiPageHeader, EuiText, EuiFlexGroup, @@ -149,213 +147,211 @@ export const AlertDetails: React.FunctionComponent = ({ : []; return ( - - - + + } + rightSideItems={[ + , + - } - rightSideItems={[ - , - - - , - ...rightPageHeaderButtons, - ]} - /> - - - - - -

- -

-
- - {alertType.name} -
- - {uniqueActions && uniqueActions.length ? ( - <> - -

- -

-
- - - {uniqueActions.map((action, index) => ( - - - {actionTypesByTypeId[action].name ?? action} - - - ))} - - - ) : null} -
- - - - - { - if (isEnabled) { - setIsEnabled(false); - await disableAlert(alert); - // Reset dismiss if previously clicked - setDissmissAlertErrors(false); - } else { - setIsEnabled(true); - await enableAlert(alert); - } - requestRefresh(); - }} - label={ - +
, + ...rightPageHeaderButtons, + ]} + /> + + + + + +

+ +

+
+ + {alertType.name} +
+ + {uniqueActions && uniqueActions.length ? ( + <> + +

+ +

+
+ + + {uniqueActions.map((action, index) => ( + + + {actionTypesByTypeId[action].name ?? action} + + + ))} + + + ) : null} +
+ + + + + { + if (isEnabled) { + setIsEnabled(false); + await disableAlert(alert); + // Reset dismiss if previously clicked + setDissmissAlertErrors(false); + } else { + setIsEnabled(true); + await enableAlert(alert); } - /> - - - { - if (isMuted) { - setIsMuted(false); - await unmuteAlert(alert); - } else { - setIsMuted(true); - await muteAlert(alert); - } - requestRefresh(); - }} - label={ + requestRefresh(); + }} + label={ + + } + /> + + + { + if (isMuted) { + setIsMuted(false); + await unmuteAlert(alert); + } else { + setIsMuted(true); + await muteAlert(alert); + } + requestRefresh(); + }} + label={ + + } + /> + + + +
+ {alert.enabled && !dissmissAlertErrors && alert.executionStatus.status === 'error' ? ( + + + + + {alert.executionStatus.error?.message} + + + + + setDissmissAlertErrors(true)} + > - } - /> - - - - - {alert.enabled && !dissmissAlertErrors && alert.executionStatus.status === 'error' ? ( - - - - - {alert.executionStatus.error?.message} - - - + + + {alert.executionStatus.error?.reason === + AlertExecutionStatusErrorReasons.License && ( - setDissmissAlertErrors(true)} + target="_blank" > - + - {alert.executionStatus.error?.reason === - AlertExecutionStatusErrorReasons.License && ( - - - - - - )} - - - - - ) : null} - - - {alert.enabled ? ( - - ) : ( - <> - - -

- -

-
- - )} + )} +
+ -
-
-
+ ) : null} + + + {alert.enabled ? ( + + ) : ( + <> + + +

+ +

+
+ + )} +
+
+ + ); }; diff --git a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts index 5eece4057ac0c7..291e5a8964553f 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts @@ -10,6 +10,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { MlCommonUI } from './common_ui'; +const fixedFooterHeight = 72; // Size of EuiBottomBar more or less + export function MachineLearningDataVisualizerFileBasedProvider( { getService, getPageObjects }: FtrProviderContext, mlCommonUI: MlCommonUI @@ -131,7 +133,9 @@ export function MachineLearningDataVisualizerFileBasedProvider( }, async selectCreateFilebeatConfig() { - await testSubjects.scrollIntoView('fileDataVisFilebeatConfigLink'); + await testSubjects.scrollIntoView('fileDataVisFilebeatConfigLink', { + bottomOffset: fixedFooterHeight, + }); await testSubjects.click('fileDataVisFilebeatConfigLink'); await testSubjects.existOrFail('fileDataVisFilebeatConfigPanel'); }, From 71a57454c7f435b9e2d4e4dc26de578370802d19 Mon Sep 17 00:00:00 2001 From: debadair Date: Tue, 29 Jun 2021 18:03:36 -0700 Subject: [PATCH 022/121] [DOCS] Update xrefs to units sections in the ES guide (#103809) --- docs/settings/reporting-settings.asciidoc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 8e870ceec8947b..b1948dbf630dda 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -109,7 +109,7 @@ security is enabled, < Date: Tue, 29 Jun 2021 22:11:09 -0300 Subject: [PATCH 023/121] Port PR 3746 from ent-search (#103765) * Port the changes as is with no modifications * Fix accessibility errors * Rename variable * Fix Stylelint issues and remove unused CSS * Extract getAsLocalDatetimeString as a util function and use it everywhere * Update backend schema Also replace schema.maybe with schema.nullable. Previously assigning "Leave unassigned" value to subtitle and description caused a server error, because we were receiving null for these values that server did not expect. * Update exampleResult mock * Add tests for DisplaySettingsLogic * Add tests for ExampleSearchResultGroup * Add tests for ExampleStandoutResult * Add tests for SearchResults * Add missed null fallback type --- .../__mocks__/content_sources.mock.ts | 4 + .../applications/workplace_search/types.ts | 4 + .../get_as_local_datetime_string.test.ts | 22 +++++ .../utils/get_as_local_datetime_string.ts | 11 +++ .../workplace_search/utils/index.ts | 1 + .../display_settings/display_settings.scss | 39 ++++++++ .../display_settings_logic.test.ts | 78 +++++++++++++++ .../display_settings_logic.ts | 65 ++++++++++++- .../example_result_detail_card.tsx | 6 +- .../example_search_result_group.test.tsx | 22 +++++ .../example_search_result_group.tsx | 70 +++++++++++++- .../example_standout_result.test.tsx | 22 +++++ .../example_standout_result.tsx | 65 ++++++++++++- .../display_settings/search_results.test.tsx | 60 ++++++++++++ .../display_settings/search_results.tsx | 96 ++++++++++++++++++- .../server/routes/workplace_search/sources.ts | 8 +- 16 files changed, 561 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index 1d04504cdcbc5c..748dc6a7cbcf88 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -305,6 +305,10 @@ export const exampleResult = { urlField: 'myLink', color: '#e3e3e3', descriptionField: 'about', + typeField: 'otherType', + mediaTypeField: 'otherMediaType', + createdByField: 'otherCreatedBy', + updatedByField: 'otherUpdatedBy', detailFields: [ { fieldName: 'cats', label: 'Felines' }, { fieldName: 'dogs', label: 'Canines' }, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index bce778f90436c8..edc772b369558b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -204,6 +204,10 @@ export interface SearchResultConfig { titleField: string | null; subtitleField: string | null; descriptionField: string | null; + typeField: string | null; + mediaTypeField: string | null; + createdByField: string | null; + updatedByField: string | null; urlField: string | null; color: string; detailFields: DetailField[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.test.ts new file mode 100644 index 00000000000000..6475df7f4c3996 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.test.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. + */ + +import { getAsLocalDateTimeString } from './'; + +describe('getAsLocalDateTimeString', () => { + it('returns localized date if string can be parsed as date', () => { + const date = '2021-06-28'; + + expect(getAsLocalDateTimeString(date)).toEqual(new Date(Date.parse(date)).toLocaleString()); + }); + + it('returns null if string cannot be parsed as date', () => { + const date = 'foo'; + + expect(getAsLocalDateTimeString(date)).toEqual(null); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.ts new file mode 100644 index 00000000000000..d5ceb50d4c9af7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const getAsLocalDateTimeString = (str: string) => { + const dateValue = Date.parse(str); + return dateValue ? new Date(dateValue).toLocaleString() : null; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts index 37228cf9e7025d..2d15d6ce407b4f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts @@ -6,3 +6,4 @@ */ export { toSentenceSerial } from './to_sentence_serial'; +export { getAsLocalDateTimeString } from './get_as_local_datetime_string'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.scss index 027f36a5c5518a..e5b680c5edec3d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.scss +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.scss @@ -22,6 +22,23 @@ 0 0 20px $euiColorLightestShade; } +@mixin searchResultTag { + height: 20px; + border-radius: 2px; + display: inline-flex; + align-items: center; + padding: 0 .25rem; + background: #E9EDF2; + color: #647487; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: .075em; + white-space: nowrap; + z-index: 1; + margin-right: 5px; +} + // Wrapper .custom-source-display-settings { font-size: 16px; @@ -73,6 +90,28 @@ color: $euiColorDarkShade; } } + + &__tag { + @include searchResultTag; + } + + &__tag-content { + display: inline-flex; + height: 20px; + flex-shrink: 0; + align-items: center; + } + + &__meta { + position: relative; + z-index: 1; + display: flex; + flex-flow: row nowrap; + align-items: center; + margin-top: .5rem; + font-size: .8em; + overflow: hidden; + } } .example-result-content-placeholder { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts index 10c715c80b3d66..3e409fb9f61446 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts @@ -52,6 +52,10 @@ describe('DisplaySettingsLogic', () => { urlFieldHover: false, subtitleFieldHover: false, descriptionFieldHover: false, + typeFieldHover: false, + mediaTypeFieldHover: false, + createdByFieldHover: false, + updatedByFieldHover: false, fieldOptions: [], optionalFieldOptions: [ { @@ -182,6 +186,50 @@ describe('DisplaySettingsLogic', () => { }); }); + it('setTypeField', () => { + const TYPE = 'new type'; + DisplaySettingsLogic.actions.setServerResponseData(serverProps); + DisplaySettingsLogic.actions.setTypeField(TYPE); + + expect(DisplaySettingsLogic.values.searchResultConfig).toEqual({ + ...searchResultConfig, + typeField: TYPE, + }); + }); + + it('setMediaTypeField', () => { + const MEDIA_TYPE = 'new media type'; + DisplaySettingsLogic.actions.setServerResponseData(serverProps); + DisplaySettingsLogic.actions.setMediaTypeField(MEDIA_TYPE); + + expect(DisplaySettingsLogic.values.searchResultConfig).toEqual({ + ...searchResultConfig, + mediaTypeField: MEDIA_TYPE, + }); + }); + + it('setCreatedByField', () => { + const CREATED_BY = 'new created by'; + DisplaySettingsLogic.actions.setServerResponseData(serverProps); + DisplaySettingsLogic.actions.setCreatedByField(CREATED_BY); + + expect(DisplaySettingsLogic.values.searchResultConfig).toEqual({ + ...searchResultConfig, + createdByField: CREATED_BY, + }); + }); + + it('setUpdatedByField', () => { + const UPDATED_BY = 'new updated by'; + DisplaySettingsLogic.actions.setServerResponseData(serverProps); + DisplaySettingsLogic.actions.setUpdatedByField(UPDATED_BY); + + expect(DisplaySettingsLogic.values.searchResultConfig).toEqual({ + ...searchResultConfig, + updatedByField: UPDATED_BY, + }); + }); + it('setDetailFields', () => { const result = { destination: { @@ -286,6 +334,36 @@ describe('DisplaySettingsLogic', () => { expect(DisplaySettingsLogic.values.urlFieldHover).toEqual(!defaultValues.urlFieldHover); }); + + it('toggleTypeFieldHover', () => { + DisplaySettingsLogic.actions.toggleTypeFieldHover(); + + expect(DisplaySettingsLogic.values.typeFieldHover).toEqual(!defaultValues.typeFieldHover); + }); + + it('toggleMediaTypeFieldHover', () => { + DisplaySettingsLogic.actions.toggleMediaTypeFieldHover(); + + expect(DisplaySettingsLogic.values.mediaTypeFieldHover).toEqual( + !defaultValues.mediaTypeFieldHover + ); + }); + + it('toggleCreatedByFieldHover', () => { + DisplaySettingsLogic.actions.toggleCreatedByFieldHover(); + + expect(DisplaySettingsLogic.values.createdByFieldHover).toEqual( + !defaultValues.createdByFieldHover + ); + }); + + it('toggleUpdatedByFieldHover', () => { + DisplaySettingsLogic.actions.toggleUpdatedByFieldHover(); + + expect(DisplaySettingsLogic.values.updatedByFieldHover).toEqual( + !defaultValues.updatedByFieldHover + ); + }); }); describe('listeners', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts index 38424df724bd4e..556507d891dcb7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts @@ -55,6 +55,10 @@ interface DisplaySettingsActions { setUrlField(urlField: string): string; setSubtitleField(subtitleField: string | null): string | null; setDescriptionField(descriptionField: string | null): string | null; + setTypeField(typeField: string | null): string | null; + setMediaTypeField(mediaTypeField: string | null): string | null; + setCreatedByField(createdByField: string | null): string | null; + setUpdatedByField(updatedByField: string | null): string | null; setColorField(hex: string): string; setDetailFields(result: DropResult): { result: DropResult }; openEditDetailField(editFieldIndex: number | null): number | null; @@ -70,6 +74,10 @@ interface DisplaySettingsActions { toggleTitleFieldHover(): void; toggleSubtitleFieldHover(): void; toggleDescriptionFieldHover(): void; + toggleTypeFieldHover(): void; + toggleMediaTypeFieldHover(): void; + toggleCreatedByFieldHover(): void; + toggleUpdatedByFieldHover(): void; toggleUrlFieldHover(): void; } @@ -89,6 +97,10 @@ interface DisplaySettingsValues { urlFieldHover: boolean; subtitleFieldHover: boolean; descriptionFieldHover: boolean; + typeFieldHover: boolean; + mediaTypeFieldHover: boolean; + createdByFieldHover: boolean; + updatedByFieldHover: boolean; fieldOptions: OptionValue[]; optionalFieldOptions: OptionValue[]; availableFieldOptions: OptionValue[]; @@ -100,6 +112,10 @@ export const defaultSearchResultConfig = { subtitleField: '', descriptionField: '', urlField: '', + typeField: '', + mediaTypeField: '', + createdByField: '', + updatedByField: '', color: '#000000', detailFields: [], }; @@ -115,7 +131,11 @@ export const DisplaySettingsLogic = kea< setTitleField: (titleField: string) => titleField, setUrlField: (urlField: string) => urlField, setSubtitleField: (subtitleField: string | null) => subtitleField, - setDescriptionField: (descriptionField: string) => descriptionField, + setDescriptionField: (descriptionField: string | null) => descriptionField, + setTypeField: (typeField: string | null) => typeField, + setMediaTypeField: (mediaTypeField: string | null) => mediaTypeField, + setCreatedByField: (createdByField: string | null) => createdByField, + setUpdatedByField: (updatedByField: string | null) => updatedByField, setColorField: (hex: string) => hex, setDetailFields: (result: DropResult) => ({ result }), openEditDetailField: (editFieldIndex: number | null) => editFieldIndex, @@ -128,6 +148,10 @@ export const DisplaySettingsLogic = kea< toggleTitleFieldHover: () => true, toggleSubtitleFieldHover: () => true, toggleDescriptionFieldHover: () => true, + toggleTypeFieldHover: () => true, + toggleMediaTypeFieldHover: () => true, + toggleCreatedByFieldHover: () => true, + toggleUpdatedByFieldHover: () => true, toggleUrlFieldHover: () => true, initializeDisplaySettings: () => true, setServerData: () => true, @@ -181,6 +205,19 @@ export const DisplaySettingsLogic = kea< ...searchResultConfig, descriptionField, }), + setTypeField: (searchResultConfig, typeField) => ({ ...searchResultConfig, typeField }), + setMediaTypeField: (searchResultConfig, mediaTypeField) => ({ + ...searchResultConfig, + mediaTypeField, + }), + setCreatedByField: (searchResultConfig, createdByField) => ({ + ...searchResultConfig, + createdByField, + }), + setUpdatedByField: (searchResultConfig, updatedByField) => ({ + ...searchResultConfig, + updatedByField, + }), setColorField: (searchResultConfig, color) => ({ ...searchResultConfig, color }), setDetailFields: (searchResultConfig, { result: { destination, source } }) => { const detailFields = cloneDeep(searchResultConfig.detailFields); @@ -273,7 +310,31 @@ export const DisplaySettingsLogic = kea< descriptionFieldHover: [ false, { - toggleDescriptionFieldHover: (addFieldModalVisible) => !addFieldModalVisible, + toggleDescriptionFieldHover: (descriptionFieldHover) => !descriptionFieldHover, + }, + ], + typeFieldHover: [ + false, + { + toggleTypeFieldHover: (typeFieldHover) => !typeFieldHover, + }, + ], + mediaTypeFieldHover: [ + false, + { + toggleMediaTypeFieldHover: (mediaTypeFieldHover) => !mediaTypeFieldHover, + }, + ], + createdByFieldHover: [ + false, + { + toggleCreatedByFieldHover: (createdByFieldHover) => !createdByFieldHover, + }, + ], + updatedByFieldHover: [ + false, + { + toggleUpdatedByFieldHover: (updatedByFieldHover) => !updatedByFieldHover, }, ], }, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx index c3d56949d0fe7d..eef508b2e618f4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx @@ -13,16 +13,12 @@ import { useValues } from 'kea'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { URL_LABEL } from '../../../../constants'; +import { getAsLocalDateTimeString } from '../../../../utils'; import { CustomSourceIcon } from './custom_source_icon'; import { DisplaySettingsLogic } from './display_settings_logic'; import { TitleField } from './title_field'; -const getAsLocalDateTimeString = (str: string) => { - const dateValue = Date.parse(str); - return dateValue ? new Date(dateValue).toLocaleString() : null; -}; - export const ExampleResultDetailCard: React.FC = () => { const { sourceName, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.test.tsx index 7139ea30be137a..1835d8485664e4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.test.tsx @@ -41,4 +41,26 @@ describe('ExampleSearchResultGroup', () => { expect(wrapper.find('[data-test-subj="DefaultDescriptionLabel"]')).toHaveLength(1); }); + + it('renders optional fields if they exist in result', () => { + setMockValues({ + ...exampleResult, + exampleDocuments: [ + { + myLink: 'http://foo', + otherTitle: 'foo', + otherType: 'File', + otherMediaType: 'PDF', + otherCreatedBy: 'bar', + otherUpdatedBy: 'baz', + }, + ], + }); + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="CreatedByField"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="UpdatedByField"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="TypeField"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="MediaTypeField"]')).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx index df89eed38ae92d..95a62b06515ce7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx @@ -13,6 +13,7 @@ import { useValues } from 'kea'; import { isColorDark, hexToRgb } from '@elastic/eui'; import { DESCRIPTION_LABEL } from '../../../../constants'; +import { getAsLocalDateTimeString } from '../../../../utils'; import { CustomSourceIcon } from './custom_source_icon'; import { DisplaySettingsLogic } from './display_settings_logic'; @@ -22,10 +23,23 @@ import { TitleField } from './title_field'; export const ExampleSearchResultGroup: React.FC = () => { const { sourceName, - searchResultConfig: { titleField, subtitleField, descriptionField, color }, + searchResultConfig: { + titleField, + subtitleField, + descriptionField, + typeField, + mediaTypeField, + createdByField, + updatedByField, + color, + }, titleFieldHover, subtitleFieldHover, descriptionFieldHover, + typeFieldHover, + mediaTypeFieldHover, + createdByFieldHover, + updatedByFieldHover, exampleDocuments, } = useValues(DisplaySettingsLogic); @@ -72,6 +86,60 @@ export const ExampleSearchResultGroup: React.FC = () => { )} + {createdByField && result[createdByField] && ( +
+ Created by {result[createdByField]} +
+ )} +
+ {typeField && result[typeField] && ( +
+ + {result[typeField]} + +
+ )} + {mediaTypeField && result[mediaTypeField] && ( +
+ + {result[mediaTypeField]} + +
+ )} +
+ + Last updated  + {updatedByField && result[updatedByField] && ( + + {' '} + by {result[updatedByField]}  + + )} + {getAsLocalDateTimeString(result.last_updated as string) || + result.last_updated} + +
+
))} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.test.tsx index a38e0ce82490d6..0c40db0c5a8ea0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.test.tsx @@ -41,4 +41,26 @@ describe('ExampleStandoutResult', () => { expect(wrapper.find('[data-test-subj="DefaultDescriptionLabel"]')).toHaveLength(1); }); + + it('renders optional fields if they exist in result', () => { + setMockValues({ + ...exampleResult, + exampleDocuments: [ + { + myLink: 'http://foo', + otherTitle: 'foo', + otherType: 'File', + otherMediaType: 'PDF', + otherCreatedBy: 'bar', + otherUpdatedBy: 'baz', + }, + ], + }); + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="CreatedByField"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="UpdatedByField"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="TypeField"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="MediaTypeField"]')).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx index 48c3149e622bd9..b6aa180eb65de4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx @@ -13,6 +13,7 @@ import { useValues } from 'kea'; import { isColorDark, hexToRgb } from '@elastic/eui'; import { DESCRIPTION_LABEL } from '../../../../constants'; +import { getAsLocalDateTimeString } from '../../../../utils'; import { CustomSourceIcon } from './custom_source_icon'; import { DisplaySettingsLogic } from './display_settings_logic'; @@ -22,10 +23,23 @@ import { TitleField } from './title_field'; export const ExampleStandoutResult: React.FC = () => { const { sourceName, - searchResultConfig: { titleField, subtitleField, descriptionField, color }, + searchResultConfig: { + titleField, + subtitleField, + descriptionField, + typeField, + mediaTypeField, + createdByField, + updatedByField, + color, + }, titleFieldHover, subtitleFieldHover, descriptionFieldHover, + typeFieldHover, + mediaTypeFieldHover, + createdByFieldHover, + updatedByFieldHover, exampleDocuments, } = useValues(DisplaySettingsLogic); @@ -66,6 +80,55 @@ export const ExampleStandoutResult: React.FC = () => { )} + {createdByField && result[createdByField] && ( +
+ Created by {result[createdByField]} +
+ )} +
+ {typeField && result[typeField] && ( +
+ {result[typeField]} +
+ )} + {mediaTypeField && result[mediaTypeField] && ( +
+ {result[mediaTypeField]} +
+ )} +
+ + Last updated  + {updatedByField && result[updatedByField] && ( + + {' '} + by {result[updatedByField]}  + + )} + {getAsLocalDateTimeString(result.last_updated as string) || result.last_updated} + +
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.test.tsx index cc0378b4b70db0..24ecb94df2aaaa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.test.tsx @@ -41,6 +41,10 @@ describe('SearchResults', () => { const setDescriptionField = jest.fn(); const setUrlField = jest.fn(); const setColorField = jest.fn(); + const setTypeField = jest.fn(); + const setMediaTypeField = jest.fn(); + const setCreatedByField = jest.fn(); + const setUpdatedByField = jest.fn(); beforeEach(() => { setMockActions({ @@ -52,6 +56,10 @@ describe('SearchResults', () => { setDescriptionField, setUrlField, setColorField, + setTypeField, + setMediaTypeField, + setCreatedByField, + setUpdatedByField, }); setMockValues({ searchResultConfig, @@ -103,6 +111,42 @@ describe('SearchResults', () => { expect(setDescriptionField).toHaveBeenCalledWith(searchResultConfig.descriptionField); }); + it('calls setTypeField on change', () => { + const wrapper = shallow(); + wrapper + .find('[data-test-subj="TypeFieldSelect"]') + .simulate('change', { target: { value: searchResultConfig.typeField } }); + + expect(setTypeField).toHaveBeenCalledWith(searchResultConfig.typeField); + }); + + it('calls setMediaTypeField on change', () => { + const wrapper = shallow(); + wrapper + .find('[data-test-subj="MediaTypeFieldSelect"]') + .simulate('change', { target: { value: searchResultConfig.mediaTypeField } }); + + expect(setMediaTypeField).toHaveBeenCalledWith(searchResultConfig.mediaTypeField); + }); + + it('calls setCreatedByField on change', () => { + const wrapper = shallow(); + wrapper + .find('[data-test-subj="CreatedByFieldSelect"]') + .simulate('change', { target: { value: searchResultConfig.createdByField } }); + + expect(setCreatedByField).toHaveBeenCalledWith(searchResultConfig.createdByField); + }); + + it('calls setUpdatedByField on change', () => { + const wrapper = shallow(); + wrapper + .find('[data-test-subj="UpdatedByFieldSelect"]') + .simulate('change', { target: { value: searchResultConfig.updatedByField } }); + + expect(setUpdatedByField).toHaveBeenCalledWith(searchResultConfig.updatedByField); + }); + it('handles blank fallbacks', () => { setMockValues({ searchResultConfig: { detailFields: [] }, @@ -116,9 +160,25 @@ describe('SearchResults', () => { wrapper .find('[data-test-subj="DescriptionFieldSelect"]') .simulate('change', { target: { value: LEAVE_UNASSIGNED_FIELD } }); + wrapper + .find('[data-test-subj="TypeFieldSelect"]') + .simulate('change', { target: { value: LEAVE_UNASSIGNED_FIELD } }); + wrapper + .find('[data-test-subj="MediaTypeFieldSelect"]') + .simulate('change', { target: { value: LEAVE_UNASSIGNED_FIELD } }); + wrapper + .find('[data-test-subj="CreatedByFieldSelect"]') + .simulate('change', { target: { value: LEAVE_UNASSIGNED_FIELD } }); + wrapper + .find('[data-test-subj="UpdatedByFieldSelect"]') + .simulate('change', { target: { value: LEAVE_UNASSIGNED_FIELD } }); expect(wrapper.find('[data-test-subj="UrlFieldSelect"]').prop('value')).toEqual(''); expect(setSubtitleField).toHaveBeenCalledWith(null); expect(setDescriptionField).toHaveBeenCalledWith(null); + expect(setTypeField).toHaveBeenCalledWith(null); + expect(setMediaTypeField).toHaveBeenCalledWith(null); + expect(setCreatedByField).toHaveBeenCalledWith(null); + expect(setUpdatedByField).toHaveBeenCalledWith(null); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx index 859fb2d5d2a206..a6a0fcda0dd670 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx @@ -42,15 +42,33 @@ export const SearchResults: React.FC = () => { toggleTitleFieldHover, toggleSubtitleFieldHover, toggleDescriptionFieldHover, + toggleTypeFieldHover, + toggleMediaTypeFieldHover, + toggleCreatedByFieldHover, + toggleUpdatedByFieldHover, setTitleField, setSubtitleField, setDescriptionField, + setTypeField, + setMediaTypeField, + setCreatedByField, + setUpdatedByField, setUrlField, setColorField, } = useActions(DisplaySettingsLogic); const { - searchResultConfig: { titleField, descriptionField, subtitleField, urlField, color }, + searchResultConfig: { + titleField, + descriptionField, + subtitleField, + typeField, + mediaTypeField, + createdByField, + updatedByField, + urlField, + color, + }, fieldOptions, optionalFieldOptions, } = useValues(DisplaySettingsLogic); @@ -136,6 +154,82 @@ export const SearchResults: React.FC = () => { } /> + + + setTypeField(value === LEAVE_UNASSIGNED_FIELD ? null : value) + } + /> + + + + setMediaTypeField(value === LEAVE_UNASSIGNED_FIELD ? null : value) + } + /> + + + + setCreatedByField(value === LEAVE_UNASSIGNED_FIELD ? null : value) + } + /> + + + + setUpdatedByField(value === LEAVE_UNASSIGNED_FIELD ? null : value) + } + /> + 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 044393f65dc59c..b393ab9d1f26a2 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 @@ -45,9 +45,13 @@ const displayFieldSchema = schema.object({ const displaySettingsSchema = schema.object({ titleField: schema.maybe(schema.string()), - subtitleField: schema.maybe(schema.string()), - descriptionField: schema.maybe(schema.string()), + subtitleField: schema.nullable(schema.string()), + descriptionField: schema.nullable(schema.string()), urlField: schema.maybe(schema.string()), + typeField: schema.nullable(schema.string()), + mediaTypeField: schema.nullable(schema.string()), + createdByField: schema.nullable(schema.string()), + updatedByField: schema.nullable(schema.string()), color: schema.string(), urlFieldIsLinkable: schema.boolean(), detailFields: schema.oneOf([schema.arrayOf(displayFieldSchema), displayFieldSchema]), From ff475164edf68e7333f41e0acbe53440d304a23f Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Tue, 29 Jun 2021 21:11:58 -0400 Subject: [PATCH 024/121] Skip flaky cypress timeline test (#103779) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../cypress/integration/timelines/row_renderers.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts index ed9a7db4702d02..b3103963284b4d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts @@ -75,7 +75,7 @@ describe('Row renderers', () => { }); }); - it('Selected renderer can be disabled with one click', () => { + it.skip('Selected renderer can be disabled with one click', () => { cy.get(TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN).click({ force: true }); cy.intercept('PATCH', '/api/timeline').as('updateTimeline'); From bed5b6d8f2dd23687a17a0c2d3e636ca4ace7d29 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 29 Jun 2021 20:16:00 -0500 Subject: [PATCH 025/121] [packages] Migrate @kbn/test to Bazel (#103122) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../monorepo-packages.asciidoc | 1 + jest.config.integration.js | 9 +- package.json | 2 +- packages/BUILD.bazel | 1 + packages/kbn-es-archiver/package.json | 5 +- packages/kbn-test/BUILD.bazel | 153 ++++++++++++++++++ packages/kbn-test/jest-preset.js | 35 ++-- packages/kbn-test/jest/package.json | 4 +- packages/kbn-test/package.json | 11 +- packages/kbn-test/scripts/build.js | 80 --------- packages/kbn-test/src/index.ts | 2 +- .../__fixtures__/jest.config.js | 2 +- packages/kbn-test/tsconfig.json | 21 +-- .../actions/snapshot_policy_actions.ts | 2 +- .../edit_role_mapping_page.test.tsx | 2 +- .../json_rule_editor.test.tsx | 2 +- .../rule_editor_panel.test.tsx | 2 +- yarn.lock | 2 +- 18 files changed, 198 insertions(+), 138 deletions(-) create mode 100644 packages/kbn-test/BUILD.bazel delete mode 100644 packages/kbn-test/scripts/build.js diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 7d708e17ae1161..06e87fa1300297 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -107,6 +107,7 @@ yarn kbn watch-bazel - @kbn/std - @kbn/storybook - @kbn/telemetry-utils +- @kbn/test - @kbn/test-subj-selector - @kbn/tinymath - @kbn/ui-framework diff --git a/jest.config.integration.js b/jest.config.integration.js index b6ecb4569b643a..8ff142714eebf9 100644 --- a/jest.config.integration.js +++ b/jest.config.integration.js @@ -16,13 +16,12 @@ module.exports = { testPathIgnorePatterns: preset.testPathIgnorePatterns.filter( (pattern) => !pattern.includes('integration_tests') ), - setupFilesAfterEnv: ['/packages/kbn-test/target/jest/setup/after_env.integration.js'], + setupFilesAfterEnv: [ + '/node_modules/@kbn/test/target_node/jest/setup/after_env.integration.js', + ], reporters: [ 'default', - [ - '/packages/kbn-test/target/jest/junit_reporter', - { reportName: 'Jest Integration Tests' }, - ], + ['@kbn/test/target_node/jest/junit_reporter', { reportName: 'Jest Integration Tests' }], ], coverageReporters: !!process.env.CI ? [['json', { file: 'jest-integration.json' }]] diff --git a/package.json b/package.json index 1111179fc816b6..99dad59044bf40 100644 --- a/package.json +++ b/package.json @@ -473,7 +473,7 @@ "@kbn/spec-to-console": "link:bazel-bin/packages/kbn-spec-to-console", "@kbn/storybook": "link:bazel-bin/packages/kbn-storybook", "@kbn/telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools", - "@kbn/test": "link:packages/kbn-test", + "@kbn/test": "link:bazel-bin/packages/kbn-test", "@kbn/test-subj-selector": "link:bazel-bin/packages/kbn-test-subj-selector", "@loaders.gl/polyfills": "^2.3.5", "@microsoft/api-documenter": "7.7.2", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 38d3f28ec866b7..6ffd5ff151ac3b 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -52,6 +52,7 @@ filegroup( "//packages/kbn-std:build", "//packages/kbn-storybook:build", "//packages/kbn-telemetry-tools:build", + "//packages/kbn-test:build", "//packages/kbn-test-subj-selector:build", "//packages/kbn-tinymath:build", "//packages/kbn-ui-framework:build", diff --git a/packages/kbn-es-archiver/package.json b/packages/kbn-es-archiver/package.json index c86d94c70d7394..b55e67b82694f5 100644 --- a/packages/kbn-es-archiver/package.json +++ b/packages/kbn-es-archiver/package.json @@ -11,8 +11,5 @@ "scripts": { "kbn:bootstrap": "rm -rf target && ../../node_modules/.bin/tsc", "kbn:watch": "rm -rf target && ../../node_modules/.bin/tsc --watch" - }, - "dependencies": { - "@kbn/test": "link:../kbn-test" } -} \ No newline at end of file +} diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel new file mode 100644 index 00000000000000..28c42a4c476846 --- /dev/null +++ b/packages/kbn-test/BUILD.bazel @@ -0,0 +1,153 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@npm//@babel/cli:index.bzl", "babel") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-test" +PKG_REQUIRE_NAME = "@kbn/test" + +SOURCE_FILES = glob( + [ + "src/**/*" + ], + exclude = [ + "**/*.test.*", + "**/*.snap", + "**/__fixture__/**", + "**/__fixtures__/**", + "**/__snapshots__/**", + ] +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "jest/package.json", + "jest-preset.js", + "jest.config.js", + "README.md", + "package.json", +] + +SRC_DEPS = [ + "//packages/kbn-dev-utils", + "//packages/kbn-i18n", + "//packages/kbn-std", + "//packages/kbn-utils", + "@npm//@elastic/elasticsearch", + "@npm//axios", + "@npm//@babel/traverse", + "@npm//chance", + "@npm//del", + "@npm//enzyme", + "@npm//execa", + "@npm//exit-hook", + "@npm//form-data", + "@npm//globby", + "@npm//history", + "@npm//jest", + "@npm//jest-cli", + "@npm//jest-snapshot", + "@npm//@jest/reporters", + "@npm//joi", + "@npm//mustache", + "@npm//parse-link-header", + "@npm//prettier", + "@npm//react-dom", + "@npm//react-redux", + "@npm//react-router-dom", + "@npm//redux", + "@npm//rxjs", + "@npm//strip-ansi", + "@npm//xmlbuilder", + "@npm//xml2js", +] + +TYPES_DEPS = [ + "@npm//@types/chance", + "@npm//@types/enzyme", + "@npm//@types/history", + "@npm//@types/jest", + "@npm//@types/joi", + "@npm//@types/lodash", + "@npm//@types/mustache", + "@npm//@types/node", + "@npm//@types/parse-link-header", + "@npm//@types/prettier", + "@npm//@types/react-dom", + "@npm//@types/react-redux", + "@npm//@types/react-router-dom", + "@npm//@types/xml2js", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +babel( + name = "target_node", + data = DEPS + [ + ":srcs", + "babel.config.js", + ], + output_dir = True, + # the following arg paths includes $(execpath) as babel runs on the sandbox root + args = [ + "./%s/src" % package_name(), + "--config-file", + "./%s/babel.config.js" % package_name(), + "--out-dir", + "$(@D)", + "--extensions", + ".ts,.js,.tsx", + "--quiet" + ], +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + declaration_dir = "target_types", + emit_declaration_only = True, + incremental = True, + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = DEPS + [":target_node", ":tsc"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index 5baff607704c78..c84fe3f7a55b05 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -9,8 +9,6 @@ // For a detailed explanation regarding each configuration property, visit: // https://jestjs.io/docs/en/configuration.html -const { resolve } = require('path'); - module.exports = { // The directory where Jest should output its coverage files coverageDirectory: '/target/kibana-coverage/jest', @@ -30,13 +28,16 @@ module.exports = { moduleNameMapper: { '@elastic/eui/lib/(.*)?': '/node_modules/@elastic/eui/test-env/$1', '@elastic/eui$': '/node_modules/@elastic/eui/test-env', - '\\.module.(css|scss)$': '/packages/kbn-test/target/jest/mocks/css_module_mock.js', - '\\.(css|less|scss)$': '/packages/kbn-test/target/jest/mocks/style_mock.js', + '\\.module.(css|scss)$': + '/node_modules/@kbn/test/target_node/jest/mocks/css_module_mock.js', + '\\.(css|less|scss)$': '/node_modules/@kbn/test/target_node/jest/mocks/style_mock.js', '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': - '/packages/kbn-test/target/jest/mocks/file_mock.js', - '\\.ace\\.worker.js$': '/packages/kbn-test/target/jest/mocks/worker_module_mock.js', - '\\.editor\\.worker.js$': '/packages/kbn-test/target/jest/mocks/worker_module_mock.js', - '^(!!)?file-loader!': '/packages/kbn-test/target/jest/mocks/file_mock.js', + '/node_modules/@kbn/test/target_node/jest/mocks/file_mock.js', + '\\.ace\\.worker.js$': + '/node_modules/@kbn/test/target_node/jest/mocks/worker_module_mock.js', + '\\.editor\\.worker.js$': + '/node_modules/@kbn/test/target_node/jest/mocks/worker_module_mock.js', + '^(!!)?file-loader!': '/node_modules/@kbn/test/target_node/jest/mocks/file_mock.js', '^src/core/(.*)': '/src/core/$1', '^src/plugins/(.*)': '/src/plugins/$1', }, @@ -45,20 +46,20 @@ module.exports = { modulePathIgnorePatterns: ['__fixtures__/', 'target/'], // Use this configuration option to add custom reporters to Jest - reporters: ['default', resolve(__dirname, './target/jest/junit_reporter')], + reporters: ['default', '@kbn/test/target_node/jest/junit_reporter'], // The paths to modules that run some code to configure or set up the testing environment before each test setupFiles: [ - '/packages/kbn-test/target/jest/setup/babel_polyfill.js', - '/packages/kbn-test/target/jest/setup/polyfills.js', - '/packages/kbn-test/target/jest/setup/enzyme.js', + '/node_modules/@kbn/test/target_node/jest/setup/babel_polyfill.js', + '/node_modules/@kbn/test/target_node/jest/setup/polyfills.js', + '/node_modules/@kbn/test/target_node/jest/setup/enzyme.js', ], // A list of paths to modules that run some code to configure or set up the testing framework before each test setupFilesAfterEnv: [ - '/packages/kbn-test/target/jest/setup/setup_test.js', - '/packages/kbn-test/target/jest/setup/mocks.js', - '/packages/kbn-test/target/jest/setup/react_testing_library.js', + '/node_modules/@kbn/test/target_node/jest/setup/setup_test.js', + '/node_modules/@kbn/test/target_node/jest/setup/mocks.js', + '/node_modules/@kbn/test/target_node/jest/setup/react_testing_library.js', ], // A list of paths to snapshot serializer modules Jest should use for snapshot testing @@ -85,7 +86,7 @@ module.exports = { // A map from regular expressions to paths to transformers transform: { - '^.+\\.(js|tsx?)$': '/packages/kbn-test/target/jest/babel_transform.js', + '^.+\\.(js|tsx?)$': '/node_modules/@kbn/test/target_node/jest/babel_transform.js', '^.+\\.txt?$': 'jest-raw-loader', '^.+\\.html?$': 'jest-raw-loader', }, @@ -109,5 +110,5 @@ module.exports = { ], // A custom resolver to preserve symlinks by default - resolver: '/packages/kbn-test/target/jest/setup/preserve_symlinks_resolver.js', + resolver: '/node_modules/@kbn/test/target_node/jest/setup/preserve_symlinks_resolver.js', }; diff --git a/packages/kbn-test/jest/package.json b/packages/kbn-test/jest/package.json index c8b50f7b1b5ba6..aa0ba838736845 100644 --- a/packages/kbn-test/jest/package.json +++ b/packages/kbn-test/jest/package.json @@ -1,4 +1,4 @@ { - "main": "../target/jest", - "types": "../target/types/jest/index.d.ts" + "main": "../target_node/jest", + "types": "../target_types/jest/index.d.ts" } diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index aaff513f1591f2..c937d1e0be85c1 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -3,14 +3,9 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "./target", - "types": "./target/types", - "scripts": { - "build": "node scripts/build", - "kbn:bootstrap": "node scripts/build --source-maps", - "kbn:watch": "node scripts/build --watch --source-maps" - }, + "main": "./target_node", + "types": "./target_types", "kibana": { "devOnly": true } -} \ No newline at end of file +} diff --git a/packages/kbn-test/scripts/build.js b/packages/kbn-test/scripts/build.js deleted file mode 100644 index 0be9d96ad6d58f..00000000000000 --- a/packages/kbn-test/scripts/build.js +++ /dev/null @@ -1,80 +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. - */ - -const { resolve } = require('path'); - -const del = require('del'); -const supportsColor = require('supports-color'); -const { run, withProcRunner } = require('@kbn/dev-utils'); - -const ROOT_DIR = resolve(__dirname, '..'); -const BUILD_DIR = resolve(ROOT_DIR, 'target'); - -const padRight = (width, str) => - str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`; - -run( - async ({ log, flags }) => { - await withProcRunner(log, async (proc) => { - log.info('Deleting old output'); - await del(BUILD_DIR); - - const cwd = ROOT_DIR; - const env = { ...process.env }; - if (supportsColor.stdout) { - env.FORCE_COLOR = 'true'; - } - - log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`); - await Promise.all([ - proc.run(padRight(10, `babel`), { - cmd: 'babel', - args: [ - 'src', - '--config-file', - require.resolve('../babel.config.js'), - '--out-dir', - BUILD_DIR, - '--extensions', - '.ts,.js,.tsx', - ...(flags.watch ? ['--watch'] : ['--quiet']), - ...(!flags['source-maps'] || !!process.env.CODE_COVERAGE - ? [] - : ['--source-maps', 'inline']), - ], - wait: true, - env, - cwd, - }), - - proc.run(padRight(10, 'tsc'), { - cmd: 'tsc', - args: [ - ...(flags.watch ? ['--watch', '--preserveWatchOutput', 'true'] : []), - ...(flags['source-maps'] ? ['--declarationMap', 'true'] : []), - ], - wait: true, - env, - cwd, - }), - ]); - - log.success('Complete'); - }); - }, - { - description: 'Simple build tool for @kbn/i18n package', - flags: { - boolean: ['watch', 'source-maps'], - help: ` - --watch Run in watch mode - --source-maps Include sourcemaps - `, - }, - } -); diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index 86cbc121703ecf..dea2ec9d1035e3 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -18,7 +18,7 @@ import { // @internal export { runTestsCli, processRunTestsCliOptions, startServersCli, processStartServersCliOptions }; -// @ts-expect-error not typed yet +// @ts-ignore not typed yet // @internal export { runTests, startServers } from './functional_tests/tasks'; diff --git a/packages/kbn-test/src/jest/integration_tests/__fixtures__/jest.config.js b/packages/kbn-test/src/jest/integration_tests/__fixtures__/jest.config.js index 90d19de897ad4f..b90cc413d3f0d4 100644 --- a/packages/kbn-test/src/jest/integration_tests/__fixtures__/jest.config.js +++ b/packages/kbn-test/src/jest/integration_tests/__fixtures__/jest.config.js @@ -13,7 +13,7 @@ module.exports = { reporters: [ 'default', [ - `${REPO_ROOT}/packages/kbn-test/target/jest/junit_reporter`, + `${REPO_ROOT}/node_modules/@kbn/test/target_node/jest/junit_reporter`, { reportName: 'JUnit Reporter Integration Test', rootDirectory: resolve( diff --git a/packages/kbn-test/tsconfig.json b/packages/kbn-test/tsconfig.json index 3cb68029d74cfd..22c502f53c03c9 100644 --- a/packages/kbn-test/tsconfig.json +++ b/packages/kbn-test/tsconfig.json @@ -1,24 +1,17 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, - "outDir": "./target/types", + "incremental": true, + "outDir": "./target_types", "stripInternal": true, - "emitDeclarationOnly": true, "declaration": true, "declarationMap": true, + "emitDeclarationOnly": true, + "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../../../packages/kbn-test/src", - "types": [ - "jest", - "node" - ], + "types": ["jest", "node"] }, - "include": [ - "src/**/*", - "index.d.ts" - ], - "exclude": [ - "**/__fixtures__/**/*" - ] + "include": ["src/**/*", "index.d.ts"], + "exclude": ["**/__fixtures__/**/*"] } diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/snapshot_policy_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/snapshot_policy_actions.ts index 0a49c3cf295bd6..37e0ef17d22544 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/snapshot_policy_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/snapshot_policy_actions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { TestBed } from '@kbn/test/target/types/jest'; +import { TestBed } from '@kbn/test/target_types/jest'; import { act } from 'react-dom/test-utils'; const createSetWaitForSnapshotAction = (testBed: TestBed) => async (snapshotPolicyName: string) => { diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx index c0486ee7d0d827..b624da2cd88b41 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx @@ -8,7 +8,7 @@ // brace/ace uses the Worker class, which is not currently provided by JSDOM. // This is not required for the tests to pass, but it rather suppresses lengthy // warnings in the console which adds unnecessary noise to the test output. -import '@kbn/test/target/jest/utils/stub_web_worker'; +import '@kbn/test/target_node/jest/utils/stub_web_worker'; import React from 'react'; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx index a4c000d3246364..1cfc57323da1d9 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx @@ -10,7 +10,7 @@ import 'brace/mode/json'; // brace/ace uses the Worker class, which is not currently provided by JSDOM. // This is not required for the tests to pass, but it rather suppresses lengthy // warnings in the console which adds unnecessary noise to the test output. -import '@kbn/test/target/jest/utils/stub_web_worker'; +import '@kbn/test/target_node/jest/utils/stub_web_worker'; import { EuiCodeEditor } from '@elastic/eui'; import React from 'react'; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx index 01b4c0a0eab6d5..c0da0ae42a7263 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx @@ -8,7 +8,7 @@ // brace/ace uses the Worker class, which is not currently provided by JSDOM. // This is not required for the tests to pass, but it rather suppresses lengthy // warnings in the console which adds unnecessary noise to the test output. -import '@kbn/test/target/jest/utils/stub_web_worker'; +import '@kbn/test/target_node/jest/utils/stub_web_worker'; import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; diff --git a/yarn.lock b/yarn.lock index 21a7b445d43712..589f07e7a35d83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2789,7 +2789,7 @@ version "0.0.0" uid "" -"@kbn/test@link:packages/kbn-test": +"@kbn/test@link:bazel-bin/packages/kbn-test": version "0.0.0" uid "" From ba5d5cf441008d9fa1b973f435bd36ef0cc4965c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ece=20=C3=96zalp?= Date: Tue, 29 Jun 2021 21:28:44 -0400 Subject: [PATCH 026/121] fixes diffing on AlertsUtilityBar (#103746) --- .../components/alerts_table/alerts_utility_bar/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx index bda8c85ddb315b..1ef79a64f831e6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx @@ -258,6 +258,5 @@ export const AlertsUtilityBar = React.memo( prevProps.totalCount === nextProps.totalCount && prevProps.showClearSelection === nextProps.showClearSelection && prevProps.showBuildingBlockAlerts === nextProps.showBuildingBlockAlerts && - prevProps.onShowOnlyThreatIndicatorAlertsChanged === - nextProps.onShowOnlyThreatIndicatorAlertsChanged + prevProps.showOnlyThreatIndicatorAlerts === nextProps.showOnlyThreatIndicatorAlerts ); From b7ad0c9004cac3399c73dd4d7a38285fce238d0e Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Tue, 29 Jun 2021 22:34:50 -0400 Subject: [PATCH 027/121] [storybook] Ignore TS-related HMR warnings (#103605) * [storybook] Ignore TS-related HMR warnings * Fix casing * Remove warnings filter Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ignore_not_found_export_plugin.ts | 36 +++++++++++++++++++ packages/kbn-storybook/webpack.config.ts | 3 +- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 packages/kbn-storybook/ignore_not_found_export_plugin.ts diff --git a/packages/kbn-storybook/ignore_not_found_export_plugin.ts b/packages/kbn-storybook/ignore_not_found_export_plugin.ts new file mode 100644 index 00000000000000..18769416f43c1a --- /dev/null +++ b/packages/kbn-storybook/ignore_not_found_export_plugin.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +// derived from https://github.com/TypeStrong/ts-loader/issues/653#issuecomment-390889335 +// +// This plugin suppresses the irritating TS-related warnings in Storybook HMR. + +import { Compiler, Stats } from 'webpack'; +// @ts-expect-error +import ModuleDependencyWarning from 'webpack/lib/ModuleDependencyWarning'; + +export class IgnoreNotFoundExportPlugin { + apply(compiler: Compiler) { + const messageRegExp = /export '.*'( \(reexported as '.*'\))? was not found in/; + + function doneHook(stats: Stats) { + stats.compilation.warnings = stats.compilation.warnings.filter(function (warn) { + if (warn instanceof ModuleDependencyWarning && messageRegExp.test(warn.message)) { + return false; + } + return true; + }); + } + + if (compiler.hooks) { + compiler.hooks.done.tap('IgnoreNotFoundExportPlugin', doneHook); + } else { + compiler.plugin('done', doneHook); + } + } +} diff --git a/packages/kbn-storybook/webpack.config.ts b/packages/kbn-storybook/webpack.config.ts index 41d3ee1f7ee5c3..97fbf40468429d 100644 --- a/packages/kbn-storybook/webpack.config.ts +++ b/packages/kbn-storybook/webpack.config.ts @@ -12,6 +12,7 @@ import { resolve } from 'path'; import { Configuration, Stats } from 'webpack'; import webpackMerge from 'webpack-merge'; import { REPO_ROOT } from './lib/constants'; +import { IgnoreNotFoundExportPlugin } from './ignore_not_found_export_plugin'; const stats = { ...Stats.presetToOptions('minimal'), @@ -19,7 +20,6 @@ const stats = { errorDetails: true, errors: true, moduleTrace: true, - warningsFilter: /(export .* was not found in)|(entrypoint size limit)/, }; // Extend the Storybook Webpack config with some customizations @@ -70,6 +70,7 @@ export default function ({ config: storybookConfig }: { config: Configuration }) }, ], }, + plugins: [new IgnoreNotFoundExportPlugin()], resolve: { extensions: ['.js', '.ts', '.tsx', '.json'], mainFields: ['browser', 'main'], From bf54eec22bdcf6dc7535c046eeaafa2e1f15a2a8 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Tue, 29 Jun 2021 22:36:13 -0400 Subject: [PATCH 028/121] [labs] Update Labs Status (#103603) * [labs] Update Labs Status * Fix translations * Supply IntersectionObserver mock * Set defer fold project to not enabled by default * Update copy for labs flyout --- .../public/application/test_helpers/index.ts | 1 + .../intersection_observer_mock.ts | 47 +++++++++++++++++++ src/plugins/dashboard/server/ui_settings.ts | 2 +- src/plugins/presentation_util/common/labs.ts | 20 ++------ .../public/components/labs/project_list.tsx | 17 ++++++- .../presentation_util/public/i18n/labs.tsx | 25 +++++----- x-pack/plugins/canvas/server/ui_settings.ts | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 9 files changed, 82 insertions(+), 34 deletions(-) create mode 100644 src/plugins/dashboard/public/application/test_helpers/intersection_observer_mock.ts diff --git a/src/plugins/dashboard/public/application/test_helpers/index.ts b/src/plugins/dashboard/public/application/test_helpers/index.ts index d26eadec8f1c9c..7c8ae86074a46d 100644 --- a/src/plugins/dashboard/public/application/test_helpers/index.ts +++ b/src/plugins/dashboard/public/application/test_helpers/index.ts @@ -9,3 +9,4 @@ export { getSampleDashboardInput, getSampleDashboardPanel } from './get_sample_dashboard_input'; export { getSavedDashboardMock } from './get_saved_dashboard_mock'; export { makeDefaultServices } from './make_default_services'; +export { setupIntersectionObserverMock } from './intersection_observer_mock'; diff --git a/src/plugins/dashboard/public/application/test_helpers/intersection_observer_mock.ts b/src/plugins/dashboard/public/application/test_helpers/intersection_observer_mock.ts new file mode 100644 index 00000000000000..401ec5acdee4ee --- /dev/null +++ b/src/plugins/dashboard/public/application/test_helpers/intersection_observer_mock.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +/** + * Utility function that mocks the `IntersectionObserver` API. Necessary for components that rely + * on it, otherwise the tests will crash. Recommended to execute inside `beforeEach`. + * + * @param intersectionObserverMock - Parameter that is sent to the `Object.defineProperty` + * overwrite method. `jest.fn()` mock functions can be passed here if the goal is to not only + * mock the intersection observer, but its methods. + */ +export function setupIntersectionObserverMock({ + root = null, + rootMargin = '', + thresholds = [], + disconnect = () => null, + observe = () => null, + takeRecords = () => [], + unobserve = () => null, +} = {}): void { + class MockIntersectionObserver implements IntersectionObserver { + readonly root: Element | null = root; + readonly rootMargin: string = rootMargin; + readonly thresholds: readonly number[] = thresholds; + disconnect: () => void = disconnect; + observe: (target: Element) => void = observe; + takeRecords: () => IntersectionObserverEntry[] = takeRecords; + unobserve: (target: Element) => void = unobserve; + } + + Object.defineProperty(window, 'IntersectionObserver', { + writable: true, + configurable: true, + value: MockIntersectionObserver, + }); + + Object.defineProperty(global, 'IntersectionObserver', { + writable: true, + configurable: true, + value: MockIntersectionObserver, + }); +} diff --git a/src/plugins/dashboard/server/ui_settings.ts b/src/plugins/dashboard/server/ui_settings.ts index 34cfff0e4ef473..99eb29a27deaa8 100644 --- a/src/plugins/dashboard/server/ui_settings.ts +++ b/src/plugins/dashboard/server/ui_settings.ts @@ -20,7 +20,7 @@ export const getUISettings = (): Record> => ({ name: i18n.translate('dashboard.labs.enableUI', { defaultMessage: 'Enable labs button in Dashboard', }), - description: i18n.translate('dashboard.labs.enableUnifiedToolbarProjectDescription', { + description: i18n.translate('dashboard.labs.enableLabsDescription', { defaultMessage: 'This flag determines if the viewer has access to the Labs button, a quick way to enable and disable experimental features in Dashboard.', }), diff --git a/src/plugins/presentation_util/common/labs.ts b/src/plugins/presentation_util/common/labs.ts index d80624fe0bb99f..b958f3de0814f9 100644 --- a/src/plugins/presentation_util/common/labs.ts +++ b/src/plugins/presentation_util/common/labs.ts @@ -9,10 +9,9 @@ import { i18n } from '@kbn/i18n'; export const LABS_PROJECT_PREFIX = 'labs:'; -export const TIME_TO_PRESENT = `${LABS_PROJECT_PREFIX}presentation:timeToPresent` as const; export const DEFER_BELOW_FOLD = `${LABS_PROJECT_PREFIX}dashboard:deferBelowFold` as const; -export const projectIDs = [TIME_TO_PRESENT, DEFER_BELOW_FOLD] as const; +export const projectIDs = [DEFER_BELOW_FOLD] as const; export const environmentNames = ['kibana', 'browser', 'session'] as const; export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const; @@ -21,30 +20,17 @@ export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const; * provided to users of our solutions in Kibana. */ export const projects: { [ID in ProjectID]: ProjectConfig & { id: ID } } = { - [TIME_TO_PRESENT]: { - id: TIME_TO_PRESENT, - isActive: false, - isDisplayed: false, - environments: ['kibana', 'browser', 'session'], - name: i18n.translate('presentationUtil.labs.enableTimeToPresentProjectName', { - defaultMessage: 'Canvas Presentation UI', - }), - description: i18n.translate('presentationUtil.labs.enableUnifiedToolbarProjectDescription', { - defaultMessage: 'Enable the new presentation-oriented UI for Canvas.', - }), - solutions: ['canvas'], - }, [DEFER_BELOW_FOLD]: { id: DEFER_BELOW_FOLD, isActive: false, isDisplayed: true, environments: ['kibana', 'browser', 'session'], name: i18n.translate('presentationUtil.labs.enableDeferBelowFoldProjectName', { - defaultMessage: 'Defer loading below "the fold"', + defaultMessage: 'Defer loading panels below "the fold"', }), description: i18n.translate('presentationUtil.labs.enableDeferBelowFoldProjectDescription', { defaultMessage: - 'Any Dashboard panels below the fold-- the area hidden beyond the bottom of the window, accessed by scrolling-- will not be loaded immediately, but only when they enter the viewport', + 'Any panels below "the fold"-- the area hidden beyond the bottom of the window, accessed by scrolling-- will not be loaded immediately, but only when they enter the viewport', }), solutions: ['dashboard'], }, diff --git a/src/plugins/presentation_util/public/components/labs/project_list.tsx b/src/plugins/presentation_util/public/components/labs/project_list.tsx index 301fd1aa6414f5..ee1997b5ca7d8e 100644 --- a/src/plugins/presentation_util/public/components/labs/project_list.tsx +++ b/src/plugins/presentation_util/public/components/labs/project_list.tsx @@ -22,7 +22,20 @@ export interface Props { onStatusChange: ProjectListItemProps['onStatusChange']; } -const EmptyList = () => ; +const EmptyList = ({ solutions }: { solutions?: SolutionName[] }) => { + let title = strings.getNoProjectsMessage(); + + if (solutions?.length === 1) { + const solution = solutions[0]; + switch (solution) { + case 'dashboard': + title = strings.getNoProjectsInSolutionMessage('Dashboard'); + case 'canvas': + title = strings.getNoProjectsInSolutionMessage('Canvas'); + } + } + return ; +}; export const ProjectList = (props: Props) => { const { solutions, projects, onStatusChange } = props; @@ -48,7 +61,7 @@ export const ProjectList = (props: Props) => { return ( - {items.length > 0 ?
    {items}
: } + {items.length > 0 ?
    {items}
: }
); }; diff --git a/src/plugins/presentation_util/public/i18n/labs.tsx b/src/plugins/presentation_util/public/i18n/labs.tsx index d9e34fa43ebb78..487c6fa6641e4d 100644 --- a/src/plugins/presentation_util/public/i18n/labs.tsx +++ b/src/plugins/presentation_util/public/i18n/labs.tsx @@ -19,8 +19,7 @@ export const LabsStrings = { defaultMessage: 'Kibana', }), help: i18n.translate('presentationUtil.labs.components.kibanaSwitchHelp', { - defaultMessage: - 'Sets the corresponding Advanced Setting for this lab project; affects all Kibana users', + defaultMessage: 'Enables this lab for all Kibana users.', }), }), getBrowserSwitchText: () => ({ @@ -28,8 +27,7 @@ export const LabsStrings = { defaultMessage: 'Browser', }), help: i18n.translate('presentationUtil.labs.components.browserSwitchHelp', { - defaultMessage: - 'Enables or disables the lab project for the browser; persists between browser instances', + defaultMessage: 'Enables the lab for this browser and persists after it closes.', }), }), getSessionSwitchText: () => ({ @@ -37,21 +35,27 @@ export const LabsStrings = { defaultMessage: 'Session', }), help: i18n.translate('presentationUtil.labs.components.sessionSwitchHelp', { - defaultMessage: - 'Enables or disables the lab project for this tab; resets when the browser tab is closed', + defaultMessage: 'Enables the lab for this browser session, so it resets when it closes.', }), }), }, List: { getNoProjectsMessage: () => i18n.translate('presentationUtil.labs.components.noProjectsMessage', { - defaultMessage: 'No available lab projects', + defaultMessage: 'No labs currently available.', + }), + getNoProjectsInSolutionMessage: (solutionName: string) => + i18n.translate('presentationUtil.labs.components.noProjectsinSolutionMessage', { + defaultMessage: 'No labs currently in {solutionName}.', + values: { + solutionName, + }, }), }, ListItem: { getOverrideLegend: () => i18n.translate('presentationUtil.labs.components.overrideFlagsLabel', { - defaultMessage: 'Override flags', + defaultMessage: 'Overrides', }), getOverriddenIconTipLabel: () => i18n.translate('presentationUtil.labs.components.overridenIconTipLabel', { @@ -81,12 +85,11 @@ export const LabsStrings = { Flyout: { getTitleLabel: () => i18n.translate('presentationUtil.labs.components.titleLabel', { - defaultMessage: 'Lab projects', + defaultMessage: 'Labs', }), getDescriptionMessage: () => i18n.translate('presentationUtil.labs.components.descriptionMessage', { - defaultMessage: - 'Lab projects are features and functionality that are in-progress or experimental in nature. They can be enabled and disabled locally for your browser or tab, or in Kibana.', + defaultMessage: 'Try out our features that are in progress or experimental.', }), getResetToDefaultLabel: () => i18n.translate('presentationUtil.labs.components.resetToDefaultLabel', { diff --git a/x-pack/plugins/canvas/server/ui_settings.ts b/x-pack/plugins/canvas/server/ui_settings.ts index 75c4cc082c5576..8c7dc9a0958728 100644 --- a/x-pack/plugins/canvas/server/ui_settings.ts +++ b/x-pack/plugins/canvas/server/ui_settings.ts @@ -19,7 +19,7 @@ export const getUISettings = (): Record> => ({ name: i18n.translate('xpack.canvas.labs.enableUI', { defaultMessage: 'Enable labs button in Canvas', }), - description: i18n.translate('xpack.canvas.labs.enableUnifiedToolbarProjectDescription', { + description: i18n.translate('xpack.canvas.labs.enableLabsDescription', { defaultMessage: 'This flag determines if the viewer has access to the Labs button, a quick way to enable and disable experimental features in Canvas.', }), diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7ef74136d1c182..fb1a5026b7e01c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3320,7 +3320,6 @@ "presentationUtil.labs.components.sessionSwitchHelp": "このタブのラボプロジェクトを有効または無効にします。ブラウザータブが閉じたときにリセットします", "presentationUtil.labs.components.sessionSwitchName": "セッション", "presentationUtil.labs.components.titleLabel": "ラボプロジェクト", - "presentationUtil.labs.enableUnifiedToolbarProjectDescription": "プレゼンテーションソリューションの新しい統合ツールバー設計を有効にする", "presentationUtil.saveModalDashboard.addToDashboardLabel": "ダッシュボードに追加", "presentationUtil.saveModalDashboard.dashboardInfoTooltip": "Visualizeライブラリに追加された項目はすべてのダッシュボードで使用できます。ライブラリ項目の編集は、使用されるすべての場所に表示されます。", "presentationUtil.saveModalDashboard.existingDashboardOptionLabel": "既存", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0d5ecf17159e53..d33212d8a2696a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3342,7 +3342,6 @@ "presentationUtil.labs.components.sessionSwitchHelp": "启用或禁用此选项卡的实验室项目;在关闭浏览器选项卡时重置", "presentationUtil.labs.components.sessionSwitchName": "会话", "presentationUtil.labs.components.titleLabel": "实验室项目", - "presentationUtil.labs.enableUnifiedToolbarProjectDescription": "启用演示解决方案的新统一工具栏设计", "presentationUtil.saveModalDashboard.addToDashboardLabel": "添加到仪表板", "presentationUtil.saveModalDashboard.dashboardInfoTooltip": "添加到 Visualize 库的项目可用于所有仪表板。对库项目的编辑将显示在使用位置。", "presentationUtil.saveModalDashboard.existingDashboardOptionLabel": "现有", From 428eba425d38fc62a7129318a153cfb67e20b232 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Tue, 29 Jun 2021 19:52:02 -0700 Subject: [PATCH 029/121] [Canvas] Removes link from workpad breadcrumb (#103793) * Removes link from workpad title breadcrumb * Fixed ts error --- x-pack/plugins/canvas/public/lib/breadcrumbs.ts | 13 ++++--------- .../routes/workpad/workpad_presentation_helper.tsx | 2 +- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/canvas/public/lib/breadcrumbs.ts b/x-pack/plugins/canvas/public/lib/breadcrumbs.ts index 35a17eda8c165f..b926c306742982 100644 --- a/x-pack/plugins/canvas/public/lib/breadcrumbs.ts +++ b/x-pack/plugins/canvas/public/lib/breadcrumbs.ts @@ -7,18 +7,13 @@ import { ChromeBreadcrumb } from '../../../../../src/core/public'; -export const getBaseBreadcrumb = () => ({ +export const getBaseBreadcrumb = (): ChromeBreadcrumb => ({ text: 'Canvas', href: '#/', }); export const getWorkpadBreadcrumb = ({ name = 'Workpad', - id, -}: { name?: string; id?: string } = {}) => { - const output: ChromeBreadcrumb = { text: name }; - if (id != null) { - output.href = `#/workpad/${id}`; - } - return output; -}; +}: { name?: string } = {}): ChromeBreadcrumb => ({ + text: name, +}); diff --git a/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx b/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx index cecb8a376c2424..ccb38cd1a1e0f2 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx +++ b/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx @@ -26,7 +26,7 @@ export const WorkpadPresentationHelper: FC = ({ children }) => { useEffect(() => { services.platform.setBreadcrumbs([ getBaseBreadcrumb(), - getWorkpadBreadcrumb({ name: workpad.name, id: workpad.id }), + getWorkpadBreadcrumb({ name: workpad.name }), ]); }, [workpad.name, workpad.id, services.platform]); From 699731f25eb05c12461ce6ba893cd89ef55f825d Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Tue, 29 Jun 2021 22:05:30 -0500 Subject: [PATCH 030/121] [Security Solution] throttle package calls on initial security page load (#103570) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../pages/endpoint_hosts/store/action.ts | 10 +++----- .../pages/endpoint_hosts/store/builders.ts | 2 +- .../pages/endpoint_hosts/store/index.test.ts | 4 ++- .../pages/endpoint_hosts/store/middleware.ts | 25 ++++++++++++++----- .../pages/endpoint_hosts/store/reducer.ts | 23 ++++++++++++----- .../pages/endpoint_hosts/store/selectors.ts | 15 ++++++++--- .../management/pages/endpoint_hosts/types.ts | 2 +- .../view/trusted_apps_page.test.tsx | 6 ++++- 8 files changed, 62 insertions(+), 25 deletions(-) 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 949feb29643173..42c16e151c45d5 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 @@ -15,7 +15,6 @@ import { } from '../../../../../common/endpoint/types'; import { ServerApiError } from '../../../../common/types'; import { GetPolicyListResponse } from '../../policy/types'; -import { GetPackagesResponse } from '../../../../../../fleet/common'; import { EndpointIndexUIQueryParams, EndpointState } from '../types'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; @@ -75,10 +74,9 @@ export interface ServerCancelledPolicyItemsLoading { type: 'serverCancelledPolicyItemsLoading'; } -export interface ServerReturnedEndpointPackageInfo { - type: 'serverReturnedEndpointPackageInfo'; - payload: GetPackagesResponse['response'][0]; -} +export type EndpointPackageInfoStateChanged = Action<'endpointPackageInfoStateChanged'> & { + payload: EndpointState['endpointPackageInfo']; +}; export interface ServerReturnedEndpointNonExistingPolicies { type: 'serverReturnedEndpointNonExistingPolicies'; @@ -195,7 +193,7 @@ export type EndpointAction = | ServerCancelledEndpointListLoading | ServerReturnedEndpointExistValue | ServerCancelledPolicyItemsLoading - | ServerReturnedEndpointPackageInfo + | EndpointPackageInfoStateChanged | ServerReturnedMetadataPatterns | ServerFailedToReturnMetadataPatterns | AppRequestedEndpointList 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 317b735e1169e5..5db861d18cd693 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 @@ -41,7 +41,7 @@ export const initialEndpointPageState = (): Immutable => { policyItems: [], selectedPolicyId: undefined, policyItemsLoading: false, - endpointPackageInfo: undefined, + endpointPackageInfo: createUninitialisedResourceState(), nonExistingPolicies: {}, agentPolicies: {}, endpointsExist: true, 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 68dd47362bc383..3bf625d726e5f3 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 @@ -64,7 +64,9 @@ describe('EndpointList store concerns', () => { policyItems: [], selectedPolicyId: undefined, policyItemsLoading: false, - endpointPackageInfo: undefined, + endpointPackageInfo: { + type: 'UninitialisedResourceState', + }, nonExistingPolicies: {}, agentPolicies: {}, endpointsExist: true, 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 ffeef3f7bf5e18..1a431ea88ad6a0 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 @@ -36,6 +36,8 @@ import { getLastLoadedActivityLogData, detailsData, getEndpointDetailsFlyoutView, + getIsEndpointPackageInfoPending, + getIsEndpointPackageInfoSuccessful, } from './selectors'; import { AgentIdsPendingActions, EndpointState, PolicyIds } from '../types'; import { @@ -44,7 +46,7 @@ import { sendGetAgentPolicyList, sendGetFleetAgentsWithEndpoint, } from '../../policy/store/services/ingest'; -import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../fleet/common'; +import { AGENT_POLICY_SAVED_OBJECT_TYPE, PackageListItem } from '../../../../../../fleet/common'; import { ENDPOINT_ACTION_LOG_ROUTE, HOST_METADATA_GET_ROUTE, @@ -61,7 +63,7 @@ import { import { isolateHost, unIsolateHost } from '../../../../common/lib/endpoint_isolation'; import { AppAction } from '../../../../common/store/actions'; import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables'; -import { ServerReturnedEndpointPackageInfo } from './action'; +import { EndpointPackageInfoStateChanged } from './action'; import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions'; import { EndpointDetailsTabsTypes } from '../view/details/components/endpoint_details_tabs'; @@ -593,20 +595,31 @@ const handleIsolateEndpointHost = async ( async function getEndpointPackageInfo( state: ImmutableObject, - dispatch: Dispatch, + dispatch: Dispatch, coreStart: CoreStart ) { - if (endpointPackageInfo(state)) return; + if (getIsEndpointPackageInfoPending(state) || getIsEndpointPackageInfoSuccessful(state)) return; + + dispatch({ + type: 'endpointPackageInfoStateChanged', + // Ignore will be fixed with when AsyncResourceState is refactored (#830) + // @ts-ignore + payload: createLoadingResourceState(endpointPackageInfo(state)), + }); try { const packageInfo = await sendGetEndpointSecurityPackage(coreStart.http); dispatch({ - type: 'serverReturnedEndpointPackageInfo', - payload: packageInfo, + type: 'endpointPackageInfoStateChanged', + payload: createLoadedResourceState(packageInfo), }); } catch (error) { // Ignore Errors, since this should not hinder the user's ability to use the UI logError(error); + dispatch({ + type: 'endpointPackageInfoStateChanged', + payload: createFailedResourceState(error), + }); } } 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 44c63edd8e95c5..0981d621f26f32 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 @@ -5,7 +5,11 @@ * 2.0. */ -import { EndpointDetailsActivityLogChanged, EndpointPendingActionsStateChanged } from './action'; +import { + EndpointDetailsActivityLogChanged, + EndpointPackageInfoStateChanged, + EndpointPendingActionsStateChanged, +} from './action'; import { isOnEndpointPage, hasSelectedEndpoint, @@ -65,6 +69,16 @@ const handleEndpointPendingActionsStateChanged: CaseReducer = ( + state, + action +) => { + return { + ...state, + endpointPackageInfo: action.payload, + }; +}; + /* eslint-disable-next-line complexity */ export const endpointListReducer: StateReducer = (state = initialEndpointPageState(), action) => { if (action.type === 'serverReturnedEndpointList') { @@ -231,11 +245,8 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta ...state, policyItemsLoading: false, }; - } else if (action.type === 'serverReturnedEndpointPackageInfo') { - return { - ...state, - endpointPackageInfo: action.payload, - }; + } else if (action.type === 'endpointPackageInfoStateChanged') { + return handleEndpointPackageInfoStateChanged(state, action); } else if (action.type === 'serverReturnedEndpointExistValue') { return { ...state, 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 eeb54379e8e7df..c09e4032d6222f 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 @@ -69,6 +69,16 @@ export const policyItemsLoading = (state: Immutable) => state.pol export const selectedPolicyId = (state: Immutable) => state.selectedPolicyId; export const endpointPackageInfo = (state: Immutable) => state.endpointPackageInfo; +export const getIsEndpointPackageInfoPending: ( + state: Immutable +) => boolean = createSelector(endpointPackageInfo, (packageInfo) => + isLoadingResourceState(packageInfo) +); +export const getIsEndpointPackageInfoSuccessful: ( + state: Immutable +) => boolean = createSelector(endpointPackageInfo, (packageInfo) => + isLoadedResourceState(packageInfo) +); export const isAutoRefreshEnabled = (state: Immutable) => state.isAutoRefreshEnabled; @@ -86,9 +96,8 @@ export const agentsWithEndpointsTotalError = (state: Immutable) = export const endpointsTotalError = (state: Immutable) => state.endpointsTotalError; const queryStrategyVersion = (state: Immutable) => state.queryStrategyVersion; -export const endpointPackageVersion = createSelector( - endpointPackageInfo, - (info) => info?.version ?? undefined +export const endpointPackageVersion = createSelector(endpointPackageInfo, (info) => + isLoadedResourceState(info) ? info.data.version : undefined ); export const isTransformEnabled = createSelector( 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 c985259588cb05..de213b3dbccc34 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 @@ -70,7 +70,7 @@ export interface EndpointState { /** the selected policy ID in the onboarding flow */ selectedPolicyId?: string; /** Endpoint package info */ - endpointPackageInfo?: GetPackagesResponse['response'][0]; + endpointPackageInfo: AsyncResourceState; /** Tracks the list of policies IDs used in Host metadata that may no longer exist */ nonExistingPolicies: PolicyIds['packagePolicy']; /** List of Package Policy Ids mapped to an associated Fleet Parent Agent Policy Id*/ diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index 970ade80bd8db7..5c627d1d7a8373 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -762,8 +762,9 @@ describe('When on the Trusted Apps Page', () => { }); beforeEach(() => { + const priorMockImplementation = coreStart.http.get.getMockImplementation(); // @ts-ignore - coreStart.http.get.mockImplementation(async (path, options) => { + coreStart.http.get.mockImplementation((path, options) => { if (path === TRUSTED_APPS_LIST_API) { const { page, per_page: perPage } = options.query as { page: number; per_page: number }; @@ -773,6 +774,9 @@ describe('When on the Trusted Apps Page', () => { return releaseListResponse(); } } + if (priorMockImplementation) { + return priorMockImplementation(path); + } }); }); From d809f48c602c67582def13867affd1389e3caa0c Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 29 Jun 2021 23:17:12 -0400 Subject: [PATCH 031/121] [ML] Anomaly Detection: add ability to clear warning notification from jobs list (#103608) * wip: adds clear messages endpoint * wip: clear messages and index new message for clearing * remove icon from jobs list on clear * remove unnecessary comments and fix typo * ensure clear messages has correct permissions * use cleaner ml context and add type * only show clear button with canCreateJob and if warning icon in table * fix types for job message pane Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ml/common/constants/index_patterns.ts | 1 + .../anomaly_detection_jobs/summary_job.ts | 1 + .../plugins/ml/common/types/audit_message.ts | 2 + .../components/job_messages/job_messages.tsx | 2 +- .../components/job_details/job_details.js | 10 +- .../job_details/job_messages_pane.tsx | 100 ++++++++++++++++-- .../jobs_list_view/jobs_list_view.js | 6 ++ .../services/ml_api_service/jobs.ts | 9 ++ .../job_audit_messages.d.ts | 1 + .../job_audit_messages/job_audit_messages.js | 74 ++++++++++++- x-pack/plugins/ml/server/routes/apidoc.json | 2 +- .../ml/server/routes/job_audit_messages.ts | 37 +++++++ .../schemas/job_audit_messages_schema.ts | 4 + 13 files changed, 234 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/ml/common/constants/index_patterns.ts b/x-pack/plugins/ml/common/constants/index_patterns.ts index d7d6c343e282bc..cec692217546d6 100644 --- a/x-pack/plugins/ml/common/constants/index_patterns.ts +++ b/x-pack/plugins/ml/common/constants/index_patterns.ts @@ -11,3 +11,4 @@ export const ML_ANNOTATIONS_INDEX_PATTERN = '.ml-annotations-6'; export const ML_RESULTS_INDEX_PATTERN = '.ml-anomalies-*'; export const ML_NOTIFICATION_INDEX_PATTERN = '.ml-notifications*'; +export const ML_NOTIFICATION_INDEX_02 = '.ml-notifications-000002'; diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts index e9e89a3c99771c..28071f88da9d93 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts @@ -46,6 +46,7 @@ export interface AuditMessage { highestLevel: string; highestLevelText: string; text: string; + cleared?: boolean; } export type MlSummaryJobs = MlSummaryJob[]; diff --git a/x-pack/plugins/ml/common/types/audit_message.ts b/x-pack/plugins/ml/common/types/audit_message.ts index bb7e3a53e51d75..ea50a8993634b2 100644 --- a/x-pack/plugins/ml/common/types/audit_message.ts +++ b/x-pack/plugins/ml/common/types/audit_message.ts @@ -11,8 +11,10 @@ export interface AuditMessageBase { timestamp: number; node_name: string; text?: string; + cleared?: boolean; } export interface JobMessage extends AuditMessageBase { job_id: string; + clearable?: boolean; } diff --git a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx index d0666ac2c660c3..2311807b6bbe6a 100644 --- a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx +++ b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx @@ -126,7 +126,7 @@ export const JobMessages: FC = ({ const defaultSorting = { sort: { field: 'timestamp' as const, - direction: 'asc' as const, + direction: 'desc' as const, }, }; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js index d030fe08eef3ed..9ec1d6cc6b606a 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js @@ -58,7 +58,7 @@ export class JobDetailsUI extends Component { ); } else { - const { showFullDetails, refreshJobList } = this.props; + const { showFullDetails, refreshJobList, showClearButton } = this.props; const { general, @@ -185,7 +185,13 @@ export class JobDetailsUI extends Component { name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.jobMessagesLabel', { defaultMessage: 'Job messages', }), - content: , + content: ( + + ), }, ]; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx index f808d512e1dfa7..a78a832fdb6e92 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx @@ -6,25 +6,38 @@ */ import React, { FC, useCallback, useEffect, useState } from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { ml } from '../../../../services/ml_api_service'; import { JobMessages } from '../../../../components/job_messages'; import { JobMessage } from '../../../../../../common/types/audit_message'; import { extractErrorMessage } from '../../../../../../common/util/errors'; import { useToastNotificationService } from '../../../../services/toast_notification_service'; +import { useMlApiContext } from '../../../../contexts/kibana'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; interface JobMessagesPaneProps { jobId: string; + showClearButton?: boolean; start?: string; end?: string; actionHandler?: (message: JobMessage) => void; + refreshJobList?: () => void; } export const JobMessagesPane: FC = React.memo( - ({ jobId, start, end, actionHandler }) => { + ({ jobId, start, end, actionHandler, refreshJobList, showClearButton }) => { + const canCreateJob = checkPermission('canCreateJob'); + const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(''); + const [isClearing, setIsClearing] = useState(false); + const toastNotificationService = useToastNotificationService(); + const { + jobs: { clearJobAuditMessages }, + } = useMlApiContext(); const fetchMessages = async () => { setIsLoading(true); @@ -46,18 +59,89 @@ export const JobMessagesPane: FC = React.memo( const refreshMessage = useCallback(fetchMessages, [jobId]); + // Clear messages for last 24hrs and refresh jobs list + const clearMessages = useCallback(async () => { + setIsClearing(true); + try { + await clearJobAuditMessages(jobId); + setIsClearing(false); + if (typeof refreshJobList === 'function') { + refreshJobList(); + } + } catch (e) { + setIsClearing(false); + toastNotificationService.displayErrorToast( + e, + i18n.translate('xpack.ml.jobMessages.clearJobAuditMessagesErrorTitle', { + defaultMessage: 'Error clearing job message warnings and errors', + }) + ); + } + }, [jobId]); + useEffect(() => { fetchMessages(); }, []); + const disabled = messages.length > 0 && messages[0].clearable === false; + + const clearButton = ( + + + + ); + return ( - + <> + + + {canCreateJob && showClearButton ? ( + +
+ {disabled === true ? ( + + {clearButton} + + ) : ( + + {clearButton} + + )} +
+
+ ) : null} + + + +
+ ); } ); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index bf8db538bc8ae6..4fdf1c4d3ab110 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -123,6 +123,9 @@ export class JobsListView extends Component { delete itemIdToExpandedRowMap[jobId]; this.setState({ itemIdToExpandedRowMap }); } else { + // Only show clear notifications button if job has warning icon due to auditMessage + const expandedJob = this.state.jobsSummaryList.filter((job) => job.id === jobId); + const showClearButton = expandedJob.length > 0 && expandedJob[0].auditMessage !== undefined; let itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap }; if (this.state.fullJobsList[jobId] !== undefined) { @@ -134,6 +137,7 @@ export class JobsListView extends Component { removeYourself={this.removeUpdateFunction} showFullDetails={this.props.isManagementTable !== true} refreshJobList={this.onRefreshClick} + showClearButton={showClearButton} /> ); } else { @@ -144,6 +148,7 @@ export class JobsListView extends Component { removeYourself={this.removeUpdateFunction} showFullDetails={this.props.isManagementTable !== true} refreshJobList={this.onRefreshClick} + showClearButton={showClearButton} /> ); } @@ -167,6 +172,7 @@ export class JobsListView extends Component { removeYourself={this.removeUpdateFunction} showFullDetails={this.props.isManagementTable !== true} refreshJobList={this.onRefreshClick} + showClearButton={showClearButton} /> ); } diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index 1de7cb455cc2c3..5695e3d8308902 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -160,6 +160,15 @@ export const jobsApiProvider = (httpService: HttpService) => ({ }); }, + clearJobAuditMessages(jobId: string) { + const body = JSON.stringify({ jobId }); + return httpService.http<{ success: boolean; latest_cleared: number }>({ + path: `${ML_BASE_PATH}/job_audit_messages/clear_messages`, + method: 'PUT', + body, + }); + }, + deletingJobTasks() { return httpService.http({ path: `${ML_BASE_PATH}/jobs/deleting_jobs_tasks`, diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts index 28287bfc8f1b48..60ea866978f1ac 100644 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts @@ -23,4 +23,5 @@ export function jobAuditMessagesProvider( } ) => any; getAuditMessagesSummary: (jobIds?: string[]) => any; + clearJobAuditMessages: (jobId: string) => any; }; diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js index e349462d8421d2..318c103b39636e 100644 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js @@ -5,7 +5,11 @@ * 2.0. */ -import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import { + ML_NOTIFICATION_INDEX_PATTERN, + ML_NOTIFICATION_INDEX_02, +} from '../../../common/constants/index_patterns'; +import { MESSAGE_LEVEL } from '../../../common/constants/message_levels'; import moment from 'moment'; const SIZE = 1000; @@ -35,7 +39,7 @@ const anomalyDetectorTypeFilter = { }, }; -export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { +export function jobAuditMessagesProvider({ asInternalUser, asCurrentUser }, mlClient) { // search for audit messages, // jobId is optional. without it, all jobs will be listed. // from is optional and should be a string formatted in ES time units. e.g. 12h, 1d, 7d @@ -123,7 +127,10 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { let messages = []; if (body.hits.total.value > 0) { - messages = body.hits.hits.map((hit) => hit._source); + messages = body.hits.hits.map((hit) => ({ + clearable: hit._index === ML_NOTIFICATION_INDEX_02, + ...hit._source, + })); } messages = await jobSavedObjectService.filterJobsForSpace( 'anomaly-detector', @@ -152,6 +159,11 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { }, anomalyDetectorTypeFilter, ], + must_not: { + term: { + cleared: true, + }, + }, }, }; @@ -266,6 +278,61 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { return jobMessages; } + const clearedTime = new Date().getTime(); + + // Sets 'cleared' to true for messages in the last 24hrs and index new message for clear action + async function clearJobAuditMessages(jobId) { + const newClearedMessage = { + job_id: jobId, + job_type: 'anomaly_detection', + level: MESSAGE_LEVEL.INFO, + message: 'Cleared set to true for messages in the last 24hrs.', + timestamp: clearedTime, + }; + + const query = { + bool: { + filter: [ + { + range: { + timestamp: { + gte: 'now-24h', + }, + }, + }, + { + term: { + job_id: jobId, + }, + }, + ], + }, + }; + + await Promise.all([ + asCurrentUser.updateByQuery({ + index: ML_NOTIFICATION_INDEX_02, + ignore_unavailable: true, + refresh: true, + conflicts: 'proceed', + body: { + query, + script: { + source: 'ctx._source.cleared = true', + lang: 'painless', + }, + }, + }), + asCurrentUser.index({ + index: ML_NOTIFICATION_INDEX_02, + body: newClearedMessage, + refresh: 'wait_for', + }), + ]); + + return { success: true, last_cleared: clearedTime }; + } + function levelToText(level) { return Object.keys(LEVEL)[Object.values(LEVEL).indexOf(level)]; } @@ -273,5 +340,6 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { return { getJobAuditMessages, getAuditMessagesSummary, + clearJobAuditMessages, }; } diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index ba712583f1b61e..27944b542b93fa 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -121,7 +121,7 @@ "JobAuditMessages", "GetJobAuditMessages", "GetAllJobAuditMessages", - + "ClearJobAuditMessages", "JobValidation", "EstimateBucketSpan", "CalculateModelMemoryLimit", diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts index 93d981aaa52afa..1548427797e168 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -11,6 +11,7 @@ import { jobAuditMessagesProvider } from '../models/job_audit_messages'; import { jobAuditMessagesQuerySchema, jobAuditMessagesJobIdSchema, + clearJobAuditMessagesBodySchema, } from './schemas/job_audit_messages_schema'; /** @@ -96,4 +97,40 @@ export function jobAuditMessagesRoutes({ router, routeGuard }: RouteInitializati } ) ); + + /** + * @apiGroup JobAuditMessages + * + * @api {put} /api/ml/job_audit_messages/clear_messages/{jobId} Index annotation + * @apiName ClearJobAuditMessages + * @apiDescription Clear the job audit messages. + * + * @apiSchema (body) clearJobAuditMessagesSchema + */ + router.put( + { + path: '/api/ml/job_audit_messages/clear_messages', + validate: { + body: clearJobAuditMessagesBodySchema, + }, + options: { + tags: ['access:ml:canCreateJob'], + }, + }, + routeGuard.fullLicenseAPIGuard( + async ({ client, mlClient, request, response, jobSavedObjectService }) => { + try { + const { clearJobAuditMessages } = jobAuditMessagesProvider(client, mlClient); + const { jobId } = request.body; + const resp = await clearJobAuditMessages(jobId); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + } + ) + ); } diff --git a/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts index ef7e3afa47e9c2..525ac73fde1207 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts @@ -17,3 +17,7 @@ export const jobAuditMessagesQuerySchema = schema.object({ start: schema.maybe(schema.string()), end: schema.maybe(schema.string()), }); + +export const clearJobAuditMessagesBodySchema = schema.object({ + jobId: schema.string(), +}); From 9a3a359c69a29612820ecc5b83cc3e306654171a Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Tue, 29 Jun 2021 23:28:08 -0400 Subject: [PATCH 032/121] [canvas] Restore Workpad Title/button to Home; fix mounting behavior (#103601) --- .../public/components/home/hooks/index.ts | 4 +- .../home/hooks/use_find_templates.ts | 25 +------ .../components/home/hooks/use_find_workpad.ts | 25 +------ .../home/{my_workpads => }/loading.tsx | 0 .../home/my_workpads/my_workpads.tsx | 18 +++-- .../workpad_templates/workpad_templates.tsx | 30 +++++--- .../components/toolbar/toolbar.component.tsx | 75 ++++++++++--------- .../public/components/toolbar/toolbar.scss | 5 ++ .../empty_prompt.stories.storyshot | 65 ---------------- 9 files changed, 80 insertions(+), 167 deletions(-) rename x-pack/plugins/canvas/public/components/home/{my_workpads => }/loading.tsx (100%) delete mode 100644 x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/home/hooks/index.ts b/x-pack/plugins/canvas/public/components/home/hooks/index.ts index 91e52948a7ba6b..c4267a98574906 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/index.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/index.ts @@ -9,7 +9,7 @@ export { useCloneWorkpad } from './use_clone_workpad'; export { useCreateWorkpad } from './use_create_workpad'; export { useDeleteWorkpads } from './use_delete_workpad'; export { useDownloadWorkpad } from './use_download_workpad'; -export { useFindTemplates, useFindTemplatesOnMount } from './use_find_templates'; -export { useFindWorkpads, useFindWorkpadsOnMount } from './use_find_workpad'; +export { useFindTemplates } from './use_find_templates'; +export { useFindWorkpads } from './use_find_workpad'; export { useImportWorkpad } from './use_upload_workpad'; export { useCreateFromTemplate } from './use_create_from_template'; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts index 13ee289fe98676..9364a79987908d 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts @@ -5,34 +5,11 @@ * 2.0. */ -import { useState, useCallback } from 'react'; -import useMount from 'react-use/lib/useMount'; +import { useCallback } from 'react'; import { useWorkpadService } from '../../../services'; -import { TemplateFindResponse } from '../../../services/workpad'; - -const emptyResponse = { templates: [] }; export const useFindTemplates = () => { const workpadService = useWorkpadService(); return useCallback(async () => await workpadService.findTemplates(), [workpadService]); }; - -export const useFindTemplatesOnMount = (): [boolean, TemplateFindResponse] => { - const [isMounted, setIsMounted] = useState(false); - const findTemplates = useFindTemplates(); - const [templateResponse, setTemplateResponse] = useState(emptyResponse); - - const fetchTemplates = useCallback(async () => { - const foundTemplates = await findTemplates(); - setTemplateResponse(foundTemplates || emptyResponse); - setIsMounted(true); - }, [findTemplates]); - - useMount(() => { - fetchTemplates(); - return () => setIsMounted(false); - }); - - return [isMounted, templateResponse]; -}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts index 3f8b0e6f630f5a..10352d0472e8c3 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts @@ -5,14 +5,10 @@ * 2.0. */ -import { useState, useCallback } from 'react'; -import useMount from 'react-use/lib/useMount'; +import { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import { WorkpadFindResponse } from '../../../services/workpad'; - import { useNotifyService, useWorkpadService } from '../../../services'; -const emptyResponse = { total: 0, workpads: [] }; export const useFindWorkpads = () => { const workpadService = useWorkpadService(); @@ -30,25 +26,6 @@ export const useFindWorkpads = () => { ); }; -export const useFindWorkpadsOnMount = (): [boolean, WorkpadFindResponse] => { - const [isMounted, setIsMounted] = useState(false); - const findWorkpads = useFindWorkpads(); - const [workpadResponse, setWorkpadResponse] = useState(emptyResponse); - - const fetchWorkpads = useCallback(async () => { - const foundWorkpads = await findWorkpads(); - setWorkpadResponse(foundWorkpads || emptyResponse); - setIsMounted(true); - }, [findWorkpads]); - - useMount(() => { - fetchWorkpads(); - return () => setIsMounted(false); - }); - - return [isMounted, workpadResponse]; -}; - const errors = { getFindFailureErrorMessage: () => i18n.translate('xpack.canvas.error.useFindWorkpads.findFailureErrorMessage', { diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx b/x-pack/plugins/canvas/public/components/home/loading.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx rename to x-pack/plugins/canvas/public/components/home/loading.tsx diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx index 4242e2e9d130f7..f5d41313ff5712 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx @@ -6,9 +6,9 @@ */ import React, { useState, useEffect, createContext, Dispatch, SetStateAction } from 'react'; -import { useFindWorkpadsOnMount } from './../hooks'; +import { useFindWorkpads } from './../hooks'; import { FoundWorkpad } from '../../../services/workpad'; -import { Loading } from './loading'; +import { Loading } from '../loading'; import { MyWorkpads as Component } from './my_workpads.component'; interface Context { @@ -19,12 +19,18 @@ interface Context { export const WorkpadsContext = createContext(null); export const MyWorkpads = () => { - const [isMounted, workpadResponse] = useFindWorkpadsOnMount(); - const [workpads, setWorkpads] = useState(workpadResponse.workpads); + const findWorkpads = useFindWorkpads(); + const [isMounted, setIsMounted] = useState(false); + const [workpads, setWorkpads] = useState([]); useEffect(() => { - setWorkpads(workpadResponse.workpads); - }, [workpadResponse]); + const mount = async () => { + const response = await findWorkpads(''); + setIsMounted(true); + setWorkpads(response?.workpads || []); + }; + mount(); + }, [setIsMounted, findWorkpads]); if (!isMounted) { return ; diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx index 352285e66424b7..6171c05e11c8e8 100644 --- a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx +++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx @@ -5,27 +5,33 @@ * 2.0. */ -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import React, { useState, useEffect } from 'react'; -import { useCreateFromTemplate, useFindTemplatesOnMount } from '../hooks'; +import { useCreateFromTemplate, useFindTemplates } from '../hooks'; import { WorkpadTemplates as Component } from './workpad_templates.component'; +import { CanvasTemplate } from '../../../../types'; +import { Loading } from '../loading'; export const WorkpadTemplates = () => { - const [isMounted, templateResponse] = useFindTemplatesOnMount(); + const findTemplates = useFindTemplates(); + const [isMounted, setIsMounted] = useState(false); + const [templates, setTemplates] = useState([]); + + useEffect(() => { + const mount = async () => { + const response = await findTemplates(); + setIsMounted(true); + setTemplates(response?.templates || []); + }; + mount(); + }, [setIsMounted, findTemplates]); + const onCreateWorkpad = useCreateFromTemplate(); if (!isMounted) { - return ( - - - - - - ); + return ; } - const { templates } = templateResponse; return ; }; diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx index 13cc4db7c6217c..dca549b6b38edb 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx @@ -93,43 +93,50 @@ export const Toolbar: FC = ({
{activeTray !== null && setActiveTray(null)}>{trays[activeTray]}}
- - - - - - toggleTray('pageManager')}> - {strings.getPageButtonLabel(selectedPageNumber, totalPages)} - + + + {workpadName} - = totalPages} - aria-label={strings.getNextPageAriaLabel()} - /> + + + + + + toggleTray('pageManager')}> + {strings.getPageButtonLabel(selectedPageNumber, totalPages)} + + + + = totalPages} + aria-label={strings.getNextPageAriaLabel()} + /> + + + {elementIsSelected && isWriteable && ( + + toggleTray('expression')} + data-test-subj="canvasExpressionEditorButton" + > + {strings.getEditorButtonLabel()} + + + )} + - - {elementIsSelected && isWriteable && ( - - toggleTray('expression')} - data-test-subj="canvasExpressionEditorButton" - > - {strings.getEditorButtonLabel()} - - - )}
diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss b/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss index c46a2ec7a1e220..9433fdddbc8bb2 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss @@ -20,6 +20,11 @@ } } +.canvasToolbar__home { + padding: $euiSizeM 0 $euiSizeM $euiSizeL; + height: 100%; +} + .canvasToolbar__controls { padding: $euiSizeM; height: 100%; diff --git a/x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot b/x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot deleted file mode 100644 index 39ec1e234ead54..00000000000000 --- a/x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot +++ /dev/null @@ -1,65 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots Home/Empty Prompt Empty Prompt 1`] = ` -
-
-
-
- -
- -

- Add your first workpad -

-
-
-

- Create a new workpad, start from a template, or import a workpad JSON file by dropping it here. -

-

- New to Canvas? - - - Add your first workpad - - . -

-
- -
-
-
-
-`; From 5f35d23416bd8fc99c4c5dfc72f3b922f78ed7be Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 30 Jun 2021 04:33:02 +0100 Subject: [PATCH 033/121] chore(NA): moving @kbn/es-archiver into bazel (#103770) --- .../monorepo-packages.asciidoc | 1 + package.json | 2 +- packages/BUILD.bazel | 1 + packages/kbn-es-archiver/BUILD.bazel | 100 ++++++++++++++++++ packages/kbn-es-archiver/package.json | 4 - packages/kbn-es-archiver/tsconfig.json | 2 +- yarn.lock | 2 +- 7 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 packages/kbn-es-archiver/BUILD.bazel diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 06e87fa1300297..b656405b173d82 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -77,6 +77,7 @@ yarn kbn watch-bazel - @kbn/dev-utils - @kbn/docs-utils - @kbn/es +- @kbn/es-archiver - @kbn/eslint-import-resolver-kibana - @kbn/eslint-plugin-eslint - @kbn/expect diff --git a/package.json b/package.json index 99dad59044bf40..1cdeed4189cd32 100644 --- a/package.json +++ b/package.json @@ -462,7 +462,7 @@ "@kbn/dev-utils": "link:bazel-bin/packages/kbn-dev-utils", "@kbn/docs-utils": "link:bazel-bin/packages/kbn-docs-utils", "@kbn/es": "link:bazel-bin/packages/kbn-es", - "@kbn/es-archiver": "link:packages/kbn-es-archiver", + "@kbn/es-archiver": "link:bazel-bin/packages/kbn-es-archiver", "@kbn/eslint-import-resolver-kibana": "link:bazel-bin/packages/kbn-eslint-import-resolver-kibana", "@kbn/eslint-plugin-eslint": "link:bazel-bin/packages/kbn-eslint-plugin-eslint", "@kbn/expect": "link:bazel-bin/packages/kbn-expect", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 6ffd5ff151ac3b..de7a27fd512769 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -20,6 +20,7 @@ filegroup( "//packages/kbn-dev-utils:build", "//packages/kbn-docs-utils:build", "//packages/kbn-es:build", + "//packages/kbn-es-archiver:build", "//packages/kbn-eslint-import-resolver-kibana:build", "//packages/kbn-eslint-plugin-eslint:build", "//packages/kbn-expect:build", diff --git a/packages/kbn-es-archiver/BUILD.bazel b/packages/kbn-es-archiver/BUILD.bazel new file mode 100644 index 00000000000000..9b3db311afa248 --- /dev/null +++ b/packages/kbn-es-archiver/BUILD.bazel @@ -0,0 +1,100 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-es-archiver" +PKG_REQUIRE_NAME = "@kbn/es-archiver" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/__fixtures__", + "**/__mocks__", + "**/__snapshots__" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +SRC_DEPS = [ + "//packages/kbn-dev-utils", + "//packages/kbn-test", + "//packages/kbn-utils", + "@npm//@elastic/elasticsearch", + "@npm//aggregate-error", + "@npm//bluebird", + "@npm//chance", + "@npm//globby", + "@npm//json-stable-stringify", + "@npm//lodash", + "@npm//sinon", + "@npm//zlib", +] + +TYPES_DEPS = [ + "@npm//@types/bluebird", + "@npm//@types/chance", + "@npm//@types/jest", + "@npm//@types/json-stable-stringify", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/sinon", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = DEPS + [":tsc"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-es-archiver/package.json b/packages/kbn-es-archiver/package.json index b55e67b82694f5..e8eb7b5f8f1c98 100644 --- a/packages/kbn-es-archiver/package.json +++ b/packages/kbn-es-archiver/package.json @@ -7,9 +7,5 @@ "types": "target/index.d.ts", "kibana": { "devOnly": true - }, - "scripts": { - "kbn:bootstrap": "rm -rf target && ../../node_modules/.bin/tsc", - "kbn:watch": "rm -rf target && ../../node_modules/.bin/tsc --watch" } } diff --git a/packages/kbn-es-archiver/tsconfig.json b/packages/kbn-es-archiver/tsconfig.json index 0950cd39d0bee4..1bc93908a993e4 100644 --- a/packages/kbn-es-archiver/tsconfig.json +++ b/packages/kbn-es-archiver/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "./target", "target": "ES2019", "declaration": true, diff --git a/yarn.lock b/yarn.lock index 589f07e7a35d83..10cbb9789ba984 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2645,7 +2645,7 @@ version "0.0.0" uid "" -"@kbn/es-archiver@link:packages/kbn-es-archiver": +"@kbn/es-archiver@link:bazel-bin/packages/kbn-es-archiver": version "0.0.0" uid "" From 0324a06bd92777834e760c924e73afa764d708d8 Mon Sep 17 00:00:00 2001 From: Mat Schaffer Date: Wed, 30 Jun 2021 13:16:13 +0900 Subject: [PATCH 034/121] Update format_number time test for APJ timezones (#102691) * Update format_number test for APJ timezones * Switch asertion to optional leading 1 * Allow leading 1 or 2 In EMEA timezones H:mm:ss can return 20:42:17 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/monitoring/public/lib/format_number.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/monitoring/public/lib/format_number.test.js b/x-pack/plugins/monitoring/public/lib/format_number.test.js index 551ec1e6a40962..b93f9101a4df2f 100644 --- a/x-pack/plugins/monitoring/public/lib/format_number.test.js +++ b/x-pack/plugins/monitoring/public/lib/format_number.test.js @@ -32,7 +32,7 @@ describe('format_number', () => { }); it('should format time in H:mm:ss', () => { - expect(formatNumber(1461868937000, 'time')).to.match(/\d\d:\d\d:\d\d/); + expect(formatNumber(1461868937000, 'time')).to.match(/[12]?\d:\d\d:\d\d/); }); it('should format integers with commas', () => { From 0fb21c49b9bde076d653d730ac6e98c7b28da7da Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 29 Jun 2021 21:47:38 -0700 Subject: [PATCH 035/121] Adds ECS guide to doc links service (#102246) --- .../public/kibana-plugin-core-public.doclinksstart.links.md | 3 +++ .../core/public/kibana-plugin-core-public.doclinksstart.md | 2 +- src/core/public/doc_links/doc_links_service.ts | 6 ++++++ src/core/public/public.api.md | 3 +++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 63d791db452d04..8754d19e2fc138 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -201,5 +201,8 @@ readonly links: { upgradeElasticAgent: string; upgradeElasticAgent712lower: string; }>; + readonly ecs: { + readonly guide: string; + }; }; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 947eece498130e..7dbb8a58694853 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
readonly ecs: {
readonly guide: string;
};
} | | diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 9206a4d1b99f1e..fcc12f43ec5311 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -418,6 +418,9 @@ export class DocLinksService { upgradeElasticAgent: `${FLEET_DOCS}upgrade-elastic-agent.html`, upgradeElasticAgent712lower: `${FLEET_DOCS}upgrade-elastic-agent.html#upgrade-7.12-lower`, }, + ecs: { + guide: `${ELASTIC_WEBSITE_URL}guide/en/ecs/current/index.html`, + }, }, }); } @@ -621,5 +624,8 @@ export interface DocLinksStart { upgradeElasticAgent: string; upgradeElasticAgent712lower: string; }>; + readonly ecs: { + readonly guide: string; + }; }; } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 8b87c21e22fa47..3f8184bea97fee 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -680,6 +680,9 @@ export interface DocLinksStart { upgradeElasticAgent: string; upgradeElasticAgent712lower: string; }>; + readonly ecs: { + readonly guide: string; + }; }; } From 569c209f2c4ceca903d0c7e68b9611dc151601ee Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 30 Jun 2021 00:16:24 -0500 Subject: [PATCH 036/121] [Security Solution][CTI] Investigation time enrichment UI (#103383) * Add pure fn and consuming hook to fetch event enrichment It's not being invoked yet, but I've added a placeholder where it's going. * Move existing enrichment tests to new spec file This is a rough copy/paste, I'll clean up as I flesh out the new tests. * Move test constants into tests that use them * style: declare FC function as an FC * Extract some inline parsing logic into a helper function And test it! * Solidifying enrichment types on the backend * Declares an enum for our types * Sets type during indicator match rule enrichment * Sets type during investigation-time enrichment * WIP: Enrichment rows are rendered on the alerts summary There are lots of TODOs here, but this implements the following: * Fetching investigation-time enrichments from the backend * Parsing existing enrichments from timeline data * Merging the two enrichment types together, and rendering them in rows as specified Much of the data-fetching is hardcoded, and this broke the existing pattern with SummaryView/SummaryRow so that got a little messy; I may end up just using my own EuiTable but we'll see. Threat Intel tab is currently broken; that's up next. * Updates ThreatDetailsView to accept an array of enrichments The investigation-time enrichments are a little messy because they contain all the non-ECS fields that indicators contain; other than that, this is looking good. Still need to add the new header, and potentially sort the fields. * Sort our details fields This promotes sanity for the user. * Add "view threat intel data" button This simply opens the threat intel tab. * Implement header for threat details sections * Add a basic jest "unit" test around ThreatSummaryView * Fix remaining tests for components we modified This also addresses a bug where we were not properly sorting new enrichments by first_seen; this is covered under the tests that were fixed. * Filter out duplicate investigation-time enrichments Because the enrichment endpoint is dumb and doesn't know about the existing event or its enrichments, we need to merge these together on the client to reduce noise and redundant data. * Add inspect button to investigation enrichments * Massages the response into the format that the inspect component uses * Moves stateful fetching of query and persisting in redux to new, more specialized hook * Moves existing enrichment hook to a more suitable location in containers/ * Fix failing unit tests * indicator match rule now specifies `matched.type` as coming from the rule * Inspecting the enrichment query requires use of the redux store, which was not previously mocked * Fix existing CTI cypress tests This covers the basics of the Alert Summary and Threat Intel tabs; the investigation-time enrichment functionality is up next. * Adds a cypress test exercising investigation time enrichment * Loads more indicators (filebeat data, `threat_indicator2` archive) AFTER the rule has executed * Asserts that those indicators are also found on the alert summary. * Populate event enrichment call with actual alert fields This was previously hardcoded during development. * Add a new field to our suspicious event to trigger enrichment The existing myhash field will generate an alert due to the way the rule is written, but the alert had no other fields that would match the investigation time enrichment. This gives it a source.ip, and updates the indicator to match. * Only fetch enrichments data if there are valid event fields If none of the alert's fields would be relevant to the enrichment query, then we don't make the request at all. * Update enrichments matched.typed in integration tests This field was updated to reflect the source of the match, in this case: indicator match rules. * Ensure draggable fields are unique in a multi-match scenario If a given field matched multiple indicators, then the previous contextId was not unique as it was based on field/value that matched. Adding provider to the mix would fix it, except that we're not guaranteed to have a provider. I've added both provider (if present) and an index value to the key to ensure that it's unique. * Simplify types This field can never be null, as we always set it in our response. * Move helper functioons out of shared location and into consuming component These are unlikely to be used elsewhere. * Clean up data parsing logic using reduce This obviates the need for our filter/guard function and the extra loop that it entails. We have to specify the return value of our reduce fn, however, but that's mostly equivalent to our type guard. * Move our general function into a general location * Extract the concept of "enrichment identifiers" This was already partially codified with 'buildEnrichmentId,' which is used to dedup enrichments; this extends the idea to all fields that could uniquely identify a given indicator. * Use existing constant as the source of our enrichments query This is now used by both the overview card and the enrichment query. * Codify our default enrichment lookback as constants * Remove unnecessary flexbox The generic SummaryView component previously had to deal with multi-valued CTI fields, representing the multiple values coming from the multiple nested objects with that field. However, with the new UI we no longer have that constraint, and so the default columnar style, and the corresponding overriding styles, are no longer necessary. * Filter out partial responses in the event enrichment observable The UI does not currently handle these. We need to test the behavior of long-running queries with this filter, but this should simplify the behavior to complete/error until we handle partial responses. * Display placeholders while event enrichment is loading Displays a loading spinner in the Threat Intel tab title, and some loading lines where the enrichments summary is. * Update our indicator data to be within the last 30 days This fixes our cypress test, but it's going to start failing again in 30 days. However, by that time I'll have implemented the absolute data picker, which will allow for a more comprehensive test in addition to us sidestepping this issue. * Fix type error with our details tabs The name prop on a Tab will be rendered as a node, so both strings and elements are acceptable. This relaxes the types to inherit from the component itself. * Fix failing jest tests The addition of our filtering of the search observable broke this test, since we now need to implement the search observable. Rather than do that, we'll instead mock our local hook as that's more likely to change. --- .../security_solution/common/cti/constants.ts | 18 +- .../security_solution/cti/index.mock.ts | 55 +- .../security_solution/cti/index.ts | 18 +- .../common/utils/data_retrieval.test.ts | 26 + .../common/utils/data_retrieval.ts | 15 + .../detection_alerts/cti_enrichments.spec.ts | 193 ++++ .../indicator_match_rule.spec.ts | 176 +--- .../cypress/screens/alerts_details.ts | 2 +- .../empty_threat_details_view.test.tsx | 10 +- .../empty_threat_details_view.tsx | 3 +- .../cti_details/enrichment_icon.tsx | 30 + .../cti_details/helpers.test.tsx | 477 ++++++++++ .../event_details/cti_details/helpers.tsx | 123 +++ .../cti_details/threat_details_view.test.tsx | 88 ++ .../cti_details/threat_details_view.tsx | 163 ++++ .../cti_details/threat_summary_view.test.tsx | 41 + .../cti_details/threat_summary_view.tsx | 141 +++ .../event_details/cti_details/translations.ts | 71 ++ .../event_details/event_details.test.tsx | 3 + .../event_details/event_details.tsx | 114 ++- .../components/event_details/helpers.tsx | 13 +- .../components/event_details/summary_view.tsx | 8 +- .../threat_details_view.test.tsx | 114 --- .../event_details/threat_details_view.tsx | 122 --- .../threat_summary_view.test.tsx | 43 - .../event_details/threat_summary_view.tsx | 78 -- .../components/event_details/translations.ts | 19 +- .../containers/cti/event_enrichment/api.ts | 54 ++ .../containers/cti/event_enrichment/index.ts | 9 + .../cti/event_enrichment/translations.ts | 15 + .../event_enrichment/use_event_enrichment.ts | 19 + .../use_investigation_enrichment.ts | 83 ++ .../enrich_signal_threat_matches.test.ts | 27 +- .../enrich_signal_threat_matches.ts | 10 +- .../factory/cti/event_enrichment/helpers.ts | 19 +- .../tests/create_threat_matching.ts | 23 +- .../suspicious_source_event/data.json | 3 + .../es_archives/threat_indicator/data.json | 1 - .../es_archives/threat_indicator2/data.json | 63 ++ .../threat_indicator2/mappings.json | 822 ++++++++++++++++++ 40 files changed, 2662 insertions(+), 650 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/utils/data_retrieval.test.ts create mode 100644 x-pack/plugins/security_solution/common/utils/data_retrieval.ts create mode 100644 x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts rename x-pack/plugins/security_solution/public/common/components/event_details/{ => cti_details}/empty_threat_details_view.test.tsx (84%) rename x-pack/plugins/security_solution/public/common/components/event_details/{ => cti_details}/empty_threat_details_view.tsx (96%) create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx create mode 100644 x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/api.ts create mode 100644 x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/translations.ts create mode 100644 x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_event_enrichment.ts create mode 100644 x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts create mode 100644 x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json create mode 100644 x-pack/test/security_solution_cypress/es_archives/threat_indicator2/mappings.json diff --git a/x-pack/plugins/security_solution/common/cti/constants.ts b/x-pack/plugins/security_solution/common/cti/constants.ts index 4e935f3e497f43..631a13df1ecb1f 100644 --- a/x-pack/plugins/security_solution/common/cti/constants.ts +++ b/x-pack/plugins/security_solution/common/cti/constants.ts @@ -9,6 +9,7 @@ import { INDICATOR_DESTINATION_PATH } from '../constants'; export const MATCHED_ATOMIC = 'matched.atomic'; export const MATCHED_FIELD = 'matched.field'; +export const MATCHED_ID = 'matched.id'; export const MATCHED_TYPE = 'matched.type'; export const INDICATOR_MATCH_SUBFIELDS = [MATCHED_ATOMIC, MATCHED_FIELD, MATCHED_TYPE]; @@ -18,11 +19,12 @@ export const INDICATOR_MATCHED_TYPE = `${INDICATOR_DESTINATION_PATH}.${MATCHED_T export const EVENT_DATASET = 'event.dataset'; export const EVENT_REFERENCE = 'event.reference'; +export const EVENT_URL = 'event.url'; export const PROVIDER = 'provider'; export const FIRSTSEEN = 'first_seen'; export const INDICATOR_DATASET = `${INDICATOR_DESTINATION_PATH}.${EVENT_DATASET}`; -export const INDICATOR_EVENT_URL = `${INDICATOR_DESTINATION_PATH}.event.url`; +export const INDICATOR_EVENT_URL = `${INDICATOR_DESTINATION_PATH}.${EVENT_URL}`; export const INDICATOR_FIRSTSEEN = `${INDICATOR_DESTINATION_PATH}.${FIRSTSEEN}`; export const INDICATOR_LASTSEEN = `${INDICATOR_DESTINATION_PATH}.last_seen`; export const INDICATOR_PROVIDER = `${INDICATOR_DESTINATION_PATH}.${PROVIDER}`; @@ -37,13 +39,10 @@ export const CTI_ROW_RENDERER_FIELDS = [ INDICATOR_PROVIDER, ]; -export const SORTED_THREAT_SUMMARY_FIELDS = [ - INDICATOR_MATCHED_FIELD, - INDICATOR_MATCHED_TYPE, - INDICATOR_PROVIDER, - INDICATOR_FIRSTSEEN, - INDICATOR_LASTSEEN, -]; +export enum ENRICHMENT_TYPES { + InvestigationTime = 'investigation_time', + IndicatorMatchRule = 'indicator_match_rule', +} export const EVENT_ENRICHMENT_INDICATOR_FIELD_MAP = { 'file.hash.md5': 'threatintel.indicator.file.hash.md5', @@ -58,6 +57,9 @@ export const EVENT_ENRICHMENT_INDICATOR_FIELD_MAP = { 'registry.path': 'threatintel.indicator.registry.path', }; +export const DEFAULT_EVENT_ENRICHMENT_FROM = 'now-30d'; +export const DEFAULT_EVENT_ENRICHMENT_TO = 'now'; + export const CTI_DEFAULT_SOURCES = [ 'Abuse URL', 'Abuse Malware', diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.mock.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.mock.ts index f3dee5a21e4c98..7898962b1a72d8 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.mock.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.mock.ts @@ -8,6 +8,7 @@ import { IEsSearchResponse } from 'src/plugins/data/public'; import { + CtiEnrichment, CtiEventEnrichmentRequestOptions, CtiEventEnrichmentStrategyResponse, CtiQueries, @@ -99,11 +100,63 @@ export const buildEventEnrichmentRawResponseMock = (): IEsSearchResponse => ({ }, }); +export const buildEventEnrichmentMock = ( + overrides: Partial = {} +): CtiEnrichment => ({ + '@timestamp': ['2021-05-28T18:33:52.993Z'], + 'agent.ephemeral_id': ['d6b14f65-5bf3-430d-8315-7b5613685979'], + 'agent.hostname': ['rylastic.local'], + 'agent.id': ['ff93aee5-86a1-4a61-b0e6-0cdc313d01b5'], + 'agent.name': ['rylastic.local'], + 'agent.type': ['filebeat'], + 'agent.version': ['8.0.0'], + 'ecs.version': ['1.6.0'], + 'event.category': ['threat'], + 'event.created': ['2021-05-28T18:33:52.993Z'], + 'event.dataset': ['threatintel.abusemalware'], + 'event.ingested': ['2021-05-28T18:33:55.086Z'], + 'event.kind': ['enrichment'], + 'event.module': ['threatintel'], + 'event.reference': [ + 'https://urlhaus-api.abuse.ch/v1/download/15b012e6f626d0f88c2926d2bf4ca394d7b8ee07cc06d2ec05ea76bed3e8a05e/', + ], + 'event.type': ['indicator'], + 'fileset.name': ['abusemalware'], + 'input.type': ['httpjson'], + 'matched.atomic': ['5529de7b60601aeb36f57824ed0e1ae8'], + 'matched.field': ['file.hash.md5'], + 'matched.id': ['31408415b6d5601a92d29b86c2519658f210c194057588ae396d55cc20b3f03d'], + 'matched.index': ['filebeat-8.0.0-2021.05.28-000001'], + 'matched.type': ['investigation_time'], + 'related.hash': [ + '5529de7b60601aeb36f57824ed0e1ae8', + '15b012e6f626d0f88c2926d2bf4ca394d7b8ee07cc06d2ec05ea76bed3e8a05e', + '768:NXSFGJ/ooP6FawrB7Bo1MWnF/jRmhJImp:1SFXIqBo1Mwj2p', + ], + 'service.type': ['threatintel'], + tags: ['threatintel-abusemalware', 'forwarded'], + 'threatintel.indicator.file.hash.md5': ['5529de7b60601aeb36f57824ed0e1ae8'], + 'threatintel.indicator.file.hash.sha256': [ + '15b012e6f626d0f88c2926d2bf4ca394d7b8ee07cc06d2ec05ea76bed3e8a05e', + ], + 'threatintel.indicator.file.hash.ssdeep': [ + '768:NXSFGJ/ooP6FawrB7Bo1MWnF/jRmhJImp:1SFXIqBo1Mwj2p', + ], + 'threatintel.indicator.file.hash.tlsh': [ + 'FFB20B82F6617061C32784E2712F7A46B179B04FD1EA54A0F28CD8E9CFE4CAA1617F1C', + ], + 'threatintel.indicator.file.size': [24738], + 'threatintel.indicator.file.type': ['html'], + 'threatintel.indicator.first_seen': ['2021-05-28T18:33:29.000Z'], + 'threatintel.indicator.type': ['file'], + ...overrides, +}); + export const buildEventEnrichmentResponseMock = ( overrides: Partial = {} ): CtiEventEnrichmentStrategyResponse => ({ ...buildEventEnrichmentRawResponseMock(), - enrichments: [], + enrichments: [buildEventEnrichmentMock()], inspect: { dsl: ['{"mocked": "json"}'] }, totalCount: 0, ...overrides, diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts index 788a44bc5b9f7f..69a6841c7c14f8 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts @@ -6,6 +6,7 @@ */ import { IEsSearchResponse } from 'src/plugins/data/public'; +import { EVENT_ENRICHMENT_INDICATOR_FIELD_MAP } from '../../../cti/constants'; import { Inspect } from '../../common'; import { RequestBasicOptions } from '..'; @@ -18,9 +19,24 @@ export interface CtiEventEnrichmentRequestOptions extends RequestBasicOptions { } export type CtiEnrichment = Record; +export type EventFields = Record; + +export interface CtiEnrichmentIdentifiers { + id: string | undefined; + field: string | undefined; + value: string | undefined; + type: string | undefined; + provider: string | undefined; +} export interface CtiEventEnrichmentStrategyResponse extends IEsSearchResponse { enrichments: CtiEnrichment[]; - inspect?: Inspect; + inspect: Inspect; totalCount: number; } + +export type EventField = keyof typeof EVENT_ENRICHMENT_INDICATOR_FIELD_MAP; +export const validEventFields = Object.keys(EVENT_ENRICHMENT_INDICATOR_FIELD_MAP) as EventField[]; + +export const isValidEventField = (field: string): field is EventField => + validEventFields.includes(field as EventField); diff --git a/x-pack/plugins/security_solution/common/utils/data_retrieval.test.ts b/x-pack/plugins/security_solution/common/utils/data_retrieval.test.ts new file mode 100644 index 00000000000000..d7ab3986a14f98 --- /dev/null +++ b/x-pack/plugins/security_solution/common/utils/data_retrieval.test.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 { getFirstElement } from './data_retrieval'; + +describe('getFirstElement', () => { + it('returns undefined if array is undefined', () => { + expect(getFirstElement(undefined)).toEqual(undefined); + }); + + it('returns undefined if array is empty', () => { + expect(getFirstElement([])).toEqual(undefined); + }); + + it('returns the first element if present', () => { + expect(getFirstElement(['hi mom'])).toEqual('hi mom'); + }); + + it('returns the first element of multiple', () => { + expect(getFirstElement(['hi mom', 'hello world'])).toEqual('hi mom'); + }); +}); diff --git a/x-pack/plugins/security_solution/common/utils/data_retrieval.ts b/x-pack/plugins/security_solution/common/utils/data_retrieval.ts new file mode 100644 index 00000000000000..04b6839b854b42 --- /dev/null +++ b/x-pack/plugins/security_solution/common/utils/data_retrieval.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. + */ + +/** + * Retrieves the first element of the given array. + * + * @param array the array to retrieve a value from + * @returns the first element of the array, or undefined if the array is undefined + */ +export const getFirstElement: (array: T[] | undefined) => T | undefined = (array) => + array ? array[0] : undefined; diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts new file mode 100644 index 00000000000000..b03daf74ce247a --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { newThreatIndicatorRule } from '../../objects/rule'; +import { cleanKibana, reload } from '../../tasks/common'; +import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; +import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; +import { + JSON_LINES, + TABLE_CELL, + TABLE_ROWS, + THREAT_CONTENT, + THREAT_DETAILS_VIEW, + THREAT_INTEL_TAB, + THREAT_SUMMARY_VIEW, + TITLE, +} from '../../screens/alerts_details'; +import { TIMELINE_FIELD } from '../../screens/rule_details'; +import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; +import { expandFirstAlert, goToManageAlertsDetectionRules } from '../../tasks/alerts'; +import { createCustomIndicatorRule } from '../../tasks/api_calls/rules'; +import { + openJsonView, + openThreatIndicatorDetails, + scrollJsonViewToBottom, +} from '../../tasks/alerts_details'; + +import { ALERTS_URL } from '../../urls/navigation'; +import { addsFieldsToTimeline } from '../../tasks/rule_details'; + +describe('CTI Enrichment', () => { + before(() => { + cleanKibana(); + esArchiverLoad('threat_indicator'); + esArchiverLoad('suspicious_source_event'); + loginAndWaitForPageWithoutDateRange(ALERTS_URL); + goToManageAlertsDetectionRules(); + createCustomIndicatorRule(newThreatIndicatorRule); + reload(); + }); + + after(() => { + esArchiverUnload('threat_indicator'); + esArchiverUnload('suspicious_source_event'); + }); + + beforeEach(() => { + loginAndWaitForPageWithoutDateRange(ALERTS_URL); + goToManageAlertsDetectionRules(); + goToRuleDetails(); + }); + + it('Displays enrichment matched.* fields on the timeline', () => { + const expectedFields = { + 'threat.indicator.matched.atomic': newThreatIndicatorRule.atomic, + 'threat.indicator.matched.type': 'indicator_match_rule', + 'threat.indicator.matched.field': newThreatIndicatorRule.indicatorMappingField, + }; + const fields = Object.keys(expectedFields) as Array; + + addsFieldsToTimeline('threat.indicator.matched', fields); + + fields.forEach((field) => { + cy.get(TIMELINE_FIELD(field)).should('have.text', expectedFields[field]); + }); + }); + + it('Displays persisted enrichments on the JSON view', () => { + const expectedEnrichment = [ + { line: 4, text: ' "threat": {' }, + { + line: 3, + text: + ' "indicator": "{\\"first_seen\\":\\"2021-03-10T08:02:14.000Z\\",\\"file\\":{\\"size\\":80280,\\"pe\\":{},\\"type\\":\\"elf\\",\\"hash\\":{\\"sha256\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"tlsh\\":\\"6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE\\",\\"ssdeep\\":\\"1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL\\",\\"md5\\":\\"9b6c3518a91d23ed77504b5416bfb5b3\\"}},\\"type\\":\\"file\\",\\"event\\":{\\"reference\\":\\"https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/\\",\\"ingested\\":\\"2021-03-10T14:51:09.809069Z\\",\\"created\\":\\"2021-03-10T14:51:07.663Z\\",\\"kind\\":\\"enrichment\\",\\"module\\":\\"threatintel\\",\\"category\\":\\"threat\\",\\"type\\":\\"indicator\\",\\"dataset\\":\\"threatintel.abusemalware\\"},\\"matched\\":{\\"atomic\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"field\\":\\"myhash.mysha256\\",\\"id\\":\\"84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f\\",\\"index\\":\\"filebeat-7.12.0-2021.03.10-000001\\",\\"type\\":\\"indicator_match_rule\\"}}"', + }, + { line: 2, text: ' }' }, + ]; + + expandFirstAlert(); + openJsonView(); + scrollJsonViewToBottom(); + + cy.get(JSON_LINES).then((elements) => { + const length = elements.length; + expectedEnrichment.forEach((enrichment) => { + cy.wrap(elements) + .eq(length - enrichment.line) + .should('have.text', enrichment.text); + }); + }); + }); + + it('Displays threat indicator details on the threat intel tab', () => { + const expectedThreatIndicatorData = [ + { field: 'event.category', value: 'threat' }, + { field: 'event.created', value: '2021-03-10T14:51:07.663Z' }, + { field: 'event.dataset', value: 'threatintel.abusemalware' }, + { field: 'event.ingested', value: '2021-03-10T14:51:09.809069Z' }, + { field: 'event.kind', value: 'enrichment' }, + { field: 'event.module', value: 'threatintel' }, + { + field: 'event.reference', + value: + 'https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/(opens in a new tab or window)', + }, + { field: 'event.type', value: 'indicator' }, + { field: 'file.hash.md5', value: '9b6c3518a91d23ed77504b5416bfb5b3' }, + { + field: 'file.hash.sha256', + value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', + }, + { + field: 'file.hash.ssdeep', + value: '1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL', + }, + { + field: 'file.hash.tlsh', + value: '6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE', + }, + { field: 'file.size', value: '80280' }, + { field: 'file.type', value: 'elf' }, + { field: 'first_seen', value: '2021-03-10T08:02:14.000Z' }, + { + field: 'matched.atomic', + value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', + }, + { field: 'matched.field', value: 'myhash.mysha256' }, + { + field: 'matched.id', + value: '84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f', + }, + { field: 'matched.index', value: 'filebeat-7.12.0-2021.03.10-000001' }, + { field: 'matched.type', value: 'indicator_match_rule' }, + { field: 'type', value: 'file' }, + ]; + + expandFirstAlert(); + openThreatIndicatorDetails(); + + cy.get(THREAT_INTEL_TAB).should('have.text', 'Threat Intel (1)'); + cy.get(THREAT_DETAILS_VIEW).within(() => { + cy.get(TABLE_ROWS).should('have.length', expectedThreatIndicatorData.length); + expectedThreatIndicatorData.forEach((row, index) => { + cy.get(TABLE_ROWS) + .eq(index) + .within(() => { + cy.get(TABLE_CELL).eq(0).should('have.text', row.field); + cy.get(TABLE_CELL).eq(1).should('have.text', row.value); + }); + }); + }); + }); + + describe('with additional indicators', () => { + before(() => { + esArchiverLoad('threat_indicator2'); + }); + + after(() => { + esArchiverUnload('threat_indicator2'); + }); + + it('Displays matched fields from both indicator match rules and investigation time enrichments on Alerts Summary tab', () => { + const indicatorMatchRuleEnrichment = { + field: 'myhash.mysha256', + value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', + }; + const investigationTimeEnrichment = { + field: 'source.ip', + value: '192.168.1.1', + }; + const expectedMatches = [indicatorMatchRuleEnrichment, investigationTimeEnrichment]; + + expandFirstAlert(); + + cy.get(THREAT_SUMMARY_VIEW).within(() => { + cy.get(TABLE_ROWS).should('have.length', expectedMatches.length); + expectedMatches.forEach((row, index) => { + cy.get(TABLE_ROWS) + .eq(index) + .within(() => { + cy.get(TITLE).should('have.text', row.field); + cy.get(THREAT_CONTENT).should('have.text', row.value); + }); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index c2e8a92474814b..e1268c52f75d4d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -16,16 +16,6 @@ import { ALERT_RULE_VERSION, NUMBER_OF_ALERTS, } from '../../screens/alerts'; -import { - JSON_LINES, - TABLE_CELL, - TABLE_ROWS, - THREAT_CONTENT, - THREAT_DETAILS_VIEW, - THREAT_INTEL_TAB, - THREAT_SUMMARY_VIEW, - TITLE, -} from '../../screens/alerts_details'; import { CUSTOM_RULES_BTN, RISK_SCORE, @@ -60,23 +50,15 @@ import { SCHEDULE_DETAILS, SEVERITY_DETAILS, TAGS_DETAILS, - TIMELINE_FIELD, TIMELINE_TEMPLATE_DETAILS, } from '../../screens/rule_details'; import { INDICATOR_MATCH_ROW_RENDER, PROVIDER_BADGE } from '../../screens/timeline'; - import { - expandFirstAlert, goToManageAlertsDetectionRules, investigateFirstAlertInTimeline, waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; -import { - openJsonView, - openThreatIndicatorDetails, - scrollJsonViewToBottom, -} from '../../tasks/alerts_details'; import { changeRowsPerPageTo100, duplicateFirstRule, @@ -121,7 +103,7 @@ import { import { goBackToRuleDetails, waitForKibana } from '../../tasks/edit_rule'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; -import { addsFieldsToTimeline, goBackToAllRulesTable } from '../../tasks/rule_details'; +import { goBackToAllRulesTable } from '../../tasks/rule_details'; import { ALERTS_URL, RULE_CREATION } from '../../urls/navigation'; @@ -520,170 +502,18 @@ describe('indicator match', () => { cy.get(PROVIDER_BADGE).should('have.length', 3); cy.get(PROVIDER_BADGE).should( 'have.text', - `threat.indicator.matched.atomic: "${newThreatIndicatorRule.atomic}"threat.indicator.matched.type: "${newThreatIndicatorRule.type}"threat.indicator.matched.field: "${newThreatIndicatorRule.indicatorMappingField}"` + `threat.indicator.matched.atomic: "${newThreatIndicatorRule.atomic}"threat.indicator.matched.type: "indicator_match_rule"threat.indicator.matched.field: "${newThreatIndicatorRule.indicatorMappingField}"` ); cy.readFile(threatIndicatorPath).then((threatIndicator) => { cy.get(INDICATOR_MATCH_ROW_RENDER).should( 'have.text', - `threat.indicator.matched.field${newThreatIndicatorRule.indicatorMappingField}${accessibilityText}matched${newThreatIndicatorRule.indicatorMappingField}${newThreatIndicatorRule.atomic}${accessibilityText}threat.indicator.matched.type${newThreatIndicatorRule.type}${accessibilityText}fromthreat.indicator.event.dataset${threatIndicator.value.source.event.dataset}${accessibilityText}:threat.indicator.event.reference${threatIndicator.value.source.event.reference}(opens in a new tab or window)${accessibilityText}` + `threat.indicator.matched.field${newThreatIndicatorRule.indicatorMappingField}${accessibilityText}matched${newThreatIndicatorRule.indicatorMappingField}${newThreatIndicatorRule.atomic}${accessibilityText}threat.indicator.matched.typeindicator_match_rule${accessibilityText}fromthreat.indicator.event.dataset${threatIndicator.value.source.event.dataset}${accessibilityText}:threat.indicator.event.reference${threatIndicator.value.source.event.reference}(opens in a new tab or window)${accessibilityText}` ); }); }); }); - describe('Enrichment', () => { - const fieldSearch = 'threat.indicator.matched'; - const fields = [ - 'threat.indicator.matched.atomic', - 'threat.indicator.matched.type', - 'threat.indicator.matched.field', - ]; - const expectedFieldsText = [ - newThreatIndicatorRule.atomic, - newThreatIndicatorRule.type, - newThreatIndicatorRule.indicatorMappingField, - ]; - - const expectedEnrichment = [ - { line: 4, text: ' "threat": {' }, - { - line: 3, - text: - ' "indicator": "{\\"first_seen\\":\\"2021-03-10T08:02:14.000Z\\",\\"file\\":{\\"size\\":80280,\\"pe\\":{},\\"type\\":\\"elf\\",\\"hash\\":{\\"sha256\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"tlsh\\":\\"6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE\\",\\"ssdeep\\":\\"1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL\\",\\"md5\\":\\"9b6c3518a91d23ed77504b5416bfb5b3\\"}},\\"type\\":\\"file\\",\\"event\\":{\\"reference\\":\\"https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/\\",\\"ingested\\":\\"2021-03-10T14:51:09.809069Z\\",\\"created\\":\\"2021-03-10T14:51:07.663Z\\",\\"kind\\":\\"enrichment\\",\\"module\\":\\"threatintel\\",\\"category\\":\\"threat\\",\\"type\\":\\"indicator\\",\\"dataset\\":\\"threatintel.abusemalware\\"},\\"matched\\":{\\"atomic\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"field\\":\\"myhash.mysha256\\",\\"id\\":\\"84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f\\",\\"index\\":\\"filebeat-7.12.0-2021.03.10-000001\\",\\"type\\":\\"file\\"}}"', - }, - { line: 2, text: ' }' }, - ]; - - before(() => { - cleanKibana(); - esArchiverLoad('threat_indicator'); - esArchiverLoad('suspicious_source_event'); - loginAndWaitForPageWithoutDateRange(ALERTS_URL); - goToManageAlertsDetectionRules(); - createCustomIndicatorRule(newThreatIndicatorRule); - reload(); - }); - - after(() => { - esArchiverUnload('threat_indicator'); - esArchiverUnload('suspicious_source_event'); - }); - - beforeEach(() => { - loginAndWaitForPageWithoutDateRange(ALERTS_URL); - goToManageAlertsDetectionRules(); - goToRuleDetails(); - }); - - it('Displays matches on the timeline', () => { - addsFieldsToTimeline(fieldSearch, fields); - - fields.forEach((field, index) => { - cy.get(TIMELINE_FIELD(field)).should('have.text', expectedFieldsText[index]); - }); - }); - - it('Displays enrichment on the JSON view', () => { - expandFirstAlert(); - openJsonView(); - scrollJsonViewToBottom(); - - cy.get(JSON_LINES).then((elements) => { - const length = elements.length; - expectedEnrichment.forEach((enrichment) => { - cy.wrap(elements) - .eq(length - enrichment.line) - .should('have.text', enrichment.text); - }); - }); - }); - - it('Displays threat summary data on alerts details', () => { - const expectedThreatSummary = [ - { field: 'matched.field', value: 'myhash.mysha256' }, - { field: 'matched.type', value: 'file' }, - { field: 'first_seen', value: '2021-03-10T08:02:14.000Z' }, - ]; - - expandFirstAlert(); - - cy.get(THREAT_SUMMARY_VIEW).within(() => { - cy.get(TABLE_ROWS).should('have.length', expectedThreatSummary.length); - expectedThreatSummary.forEach((row, index) => { - cy.get(TABLE_ROWS) - .eq(index) - .within(() => { - cy.get(TITLE).should('have.text', row.field); - cy.get(THREAT_CONTENT).should('have.text', row.value); - }); - }); - }); - }); - - it('Displays threat indicator data on the threat intel tab', () => { - const expectedThreatIndicatorData = [ - { field: 'first_seen', value: '2021-03-10T08:02:14.000Z' }, - { field: 'file.size', value: '80280' }, - { field: 'file.type', value: 'elf' }, - { - field: 'file.hash.sha256', - value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', - }, - { - field: 'file.hash.tlsh', - value: '6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE', - }, - { - field: 'file.hash.ssdeep', - value: - '1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL', - }, - { field: 'file.hash.md5', value: '9b6c3518a91d23ed77504b5416bfb5b3' }, - { field: 'type', value: 'file' }, - { - field: 'event.reference', - value: - 'https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/(opens in a new tab or window)', - }, - { field: 'event.ingested', value: '2021-03-10T14:51:09.809069Z' }, - { field: 'event.created', value: '2021-03-10T14:51:07.663Z' }, - { field: 'event.kind', value: 'enrichment' }, - { field: 'event.module', value: 'threatintel' }, - { field: 'event.category', value: 'threat' }, - { field: 'event.type', value: 'indicator' }, - { field: 'event.dataset', value: 'threatintel.abusemalware' }, - { - field: 'matched.atomic', - value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', - }, - { field: 'matched.field', value: 'myhash.mysha256' }, - { - field: 'matched.id', - value: '84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f', - }, - { field: 'matched.index', value: 'filebeat-7.12.0-2021.03.10-000001' }, - { field: 'matched.type', value: 'file' }, - ]; - - expandFirstAlert(); - openThreatIndicatorDetails(); - - cy.get(THREAT_INTEL_TAB).should('have.text', 'Threat Intel (1)'); - cy.get(THREAT_DETAILS_VIEW).within(() => { - cy.get(TABLE_ROWS).should('have.length', expectedThreatIndicatorData.length); - expectedThreatIndicatorData.forEach((row, index) => { - cy.get(TABLE_ROWS) - .eq(index) - .within(() => { - cy.get(TABLE_CELL).eq(0).should('have.text', row.field); - cy.get(TABLE_CELL).eq(1).should('have.text', row.value); - }); - }); - }); - }); - }); - describe('Duplicates the indicator rule', () => { beforeEach(() => { cleanKibana(); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts index 12bef09b8356d5..460652cf6f2daf 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts @@ -19,7 +19,7 @@ export const TABLE_TAB = '[data-test-subj="tableTab"]'; export const TABLE_ROWS = '.euiTableRow'; -export const THREAT_CONTENT = '[data-test-subj^=draggable-content-threat]'; +export const THREAT_CONTENT = '[data-test-subj^=draggable-content-]'; export const THREAT_DETAILS_VIEW = '[data-test-subj="threat-details-view-0"]'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/empty_threat_details_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.test.tsx similarity index 84% rename from x-pack/plugins/security_solution/public/common/components/event_details/empty_threat_details_view.test.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.test.tsx index b3e70fd17c0e12..76c6b077236f0d 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/empty_threat_details_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.test.tsx @@ -8,11 +8,11 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { useMountAppended } from '../../utils/use_mount_appended'; -import { getMockTheme } from '../../lib/kibana/kibana_react.mock'; +import { useMountAppended } from '../../../utils/use_mount_appended'; +import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; import { EmptyThreatDetailsView } from './empty_threat_details_view'; -jest.mock('../../lib/kibana'); +jest.mock('../../../lib/kibana'); describe('EmptyThreatDetailsView', () => { const mount = useMountAppended(); @@ -28,10 +28,6 @@ describe('EmptyThreatDetailsView', () => { }, }); - beforeEach(() => { - jest.clearAllMocks(); - }); - test('renders correct items', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/empty_threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/common/components/event_details/empty_threat_details_view.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.tsx index c78df92dceb3c1..d7e1c4d7754ecd 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/empty_threat_details_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.tsx @@ -8,8 +8,9 @@ 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'; -import { useKibana } from '../../lib/kibana'; const EmptyThreatDetailsViewContainer = styled.div` display: flex; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx new file mode 100644 index 00000000000000..042940a1cf036c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx @@ -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 React from 'react'; +import { EuiIcon, EuiToolTip } from '@elastic/eui'; + +import * as i18n from './translations'; +import { isInvestigationTimeEnrichment } from './helpers'; + +export const getTooltipTitle = (type: string | undefined) => + isInvestigationTimeEnrichment(type) + ? i18n.INVESTIGATION_TOOLTIP_TITLE + : i18n.INDICATOR_TOOLTIP_TITLE; + +export const getTooltipContent = (type: string | undefined) => + isInvestigationTimeEnrichment(type) + ? i18n.INVESTIGATION_TOOLTIP_CONTENT + : i18n.INDICATOR_TOOLTIP_CONTENT; + +export const EnrichmentIcon: React.FC<{ type: string | undefined }> = ({ type }) => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx new file mode 100644 index 00000000000000..858962efa9e83f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx @@ -0,0 +1,477 @@ +/* + * 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 { ENRICHMENT_TYPES } from '../../../../../common/cti/constants'; +import { buildEventEnrichmentMock } from '../../../../../common/search_strategy/security_solution/cti/index.mock'; +import { + filterDuplicateEnrichments, + getEnrichmentFields, + parseExistingEnrichments, +} from './helpers'; + +describe('parseExistingEnrichments', () => { + it('returns an empty array if data is empty', () => { + expect(parseExistingEnrichments([])).toEqual([]); + }); + + it('returns an empty array if data contains no enrichment field', () => { + const data = [ + { + category: 'host', + field: 'host.os.name.text', + isObjectArray: false, + originalValue: ['Mac OS X'], + values: ['Mac OS X'], + }, + ]; + expect(parseExistingEnrichments(data)).toEqual([]); + }); + + it('returns an empty array if enrichment field contains invalid JSON', () => { + const data = [ + { + category: 'threat', + field: 'threat.indicator', + isObjectArray: true, + originalValue: ['whoops'], + values: ['whoops'], + }, + ]; + expect(parseExistingEnrichments(data)).toEqual([]); + }); + + it('returns an array if enrichment field contains valid JSON', () => { + const data = [ + { + category: 'threat', + field: 'threat.indicator', + isObjectArray: true, + originalValue: [ + `{"first_seen":"2021-03-21T19:40:19.000Z","provider":"geenensp","ip":"192.168.1.19","type":"url","event":{"reference":"https://urlhaus.abuse.ch/url/1055419/","ingested":"2021-03-08T19:40:44.213673Z","created":"2021-03-08T19:40:43.160Z","kind":"other","module":"threatintel","category":"threat","type":"indicator","dataset":"threatintel.abuseurl"},"matched":{"atomic":"192.168.1.19","field":"host.ip","id":"0SIZMnoB_Blp1Ib9ZYHU","index":"filebeat-8.0.0-2021.05.28-000001","type":"url"}}`, + ], + values: [ + `{"first_seen":"2021-03-21T19:40:19.000Z","provider":"geenensp","ip":"192.168.1.19","type":"url","event":{"reference":"https://urlhaus.abuse.ch/url/1055419/","ingested":"2021-03-08T19:40:44.213673Z","created":"2021-03-08T19:40:43.160Z","kind":"other","module":"threatintel","category":"threat","type":"indicator","dataset":"threatintel.abuseurl"},"matched":{"atomic":"192.168.1.19","field":"host.ip","id":"0SIZMnoB_Blp1Ib9ZYHU","index":"filebeat-8.0.0-2021.05.28-000001","type":"url"}}`, + ], + }, + ]; + + expect(parseExistingEnrichments(data)).toEqual([ + [ + { + category: 'first_seen', + field: 'first_seen', + isObjectArray: false, + originalValue: ['2021-03-21T19:40:19.000Z'], + values: ['2021-03-21T19:40:19.000Z'], + }, + { + category: 'provider', + field: 'provider', + isObjectArray: false, + originalValue: ['geenensp'], + values: ['geenensp'], + }, + { + category: 'ip', + field: 'ip', + isObjectArray: false, + originalValue: ['192.168.1.19'], + values: ['192.168.1.19'], + }, + { + category: 'type', + field: 'type', + isObjectArray: false, + originalValue: ['url'], + values: ['url'], + }, + { + category: 'event', + field: 'event.reference', + isObjectArray: false, + originalValue: ['https://urlhaus.abuse.ch/url/1055419/'], + values: ['https://urlhaus.abuse.ch/url/1055419/'], + }, + { + category: 'event', + field: 'event.ingested', + isObjectArray: false, + originalValue: ['2021-03-08T19:40:44.213673Z'], + values: ['2021-03-08T19:40:44.213673Z'], + }, + { + category: 'event', + field: 'event.created', + isObjectArray: false, + originalValue: ['2021-03-08T19:40:43.160Z'], + values: ['2021-03-08T19:40:43.160Z'], + }, + { + category: 'event', + field: 'event.kind', + isObjectArray: false, + originalValue: ['other'], + values: ['other'], + }, + { + category: 'event', + field: 'event.module', + isObjectArray: false, + originalValue: ['threatintel'], + values: ['threatintel'], + }, + { + category: 'event', + field: 'event.category', + isObjectArray: false, + originalValue: ['threat'], + values: ['threat'], + }, + { + category: 'event', + field: 'event.type', + isObjectArray: false, + originalValue: ['indicator'], + values: ['indicator'], + }, + { + category: 'event', + field: 'event.dataset', + isObjectArray: false, + originalValue: ['threatintel.abuseurl'], + values: ['threatintel.abuseurl'], + }, + { + category: 'matched', + field: 'matched.atomic', + isObjectArray: false, + originalValue: ['192.168.1.19'], + values: ['192.168.1.19'], + }, + { + category: 'matched', + field: 'matched.field', + isObjectArray: false, + originalValue: ['host.ip'], + values: ['host.ip'], + }, + { + category: 'matched', + field: 'matched.id', + isObjectArray: false, + originalValue: ['0SIZMnoB_Blp1Ib9ZYHU'], + values: ['0SIZMnoB_Blp1Ib9ZYHU'], + }, + { + category: 'matched', + field: 'matched.index', + isObjectArray: false, + originalValue: ['filebeat-8.0.0-2021.05.28-000001'], + values: ['filebeat-8.0.0-2021.05.28-000001'], + }, + { + category: 'matched', + field: 'matched.type', + isObjectArray: false, + originalValue: ['url'], + values: ['url'], + }, + ], + ]); + }); + + it('returns multiple arrays for multiple enrichments', () => { + const data = [ + { + category: 'threat', + field: 'threat.indicator', + isObjectArray: true, + originalValue: [ + `{"first_seen":"2021-03-21T19:40:19.000Z","provider":"other","ip":"192.168.1.19","type":"url","event":{"reference":"https://urlhaus.abuse.ch/url/1055419/","ingested":"2021-03-08T19:40:44.213673Z","created":"2021-03-08T19:40:43.160Z","kind":"other","module":"threatintel","category":"threat","type":"indicator","dataset":"threatintel.abuseurl"},"matched":{"atomic":"192.168.1.19","field":"host.ip","id":"iiL9NHoB_Blp1Ib9yoJo","index":"filebeat-8.0.0-2021.05.28-000001","type":"url"}}`, + `{"first_seen":"2021-03-21T19:40:19.000Z","provider":"geenensp","ip":"192.168.1.19","type":"url","event":{"reference":"https://urlhaus.abuse.ch/url/1055419/","ingested":"2021-03-08T19:40:44.213673Z","created":"2021-03-08T19:40:43.160Z","kind":"other","module":"threatintel","category":"threat","type":"indicator","dataset":"threatintel.abuseurl"},"matched":{"atomic":"192.168.1.19","field":"host.ip","id":"0SIZMnoB_Blp1Ib9ZYHU","index":"filebeat-8.0.0-2021.05.28-000001","type":"url"}}`, + ], + values: [ + `{"first_seen":"2021-03-21T19:40:19.000Z","provider":"other","ip":"192.168.1.19","type":"url","event":{"reference":"https://urlhaus.abuse.ch/url/1055419/","ingested":"2021-03-08T19:40:44.213673Z","created":"2021-03-08T19:40:43.160Z","kind":"other","module":"threatintel","category":"threat","type":"indicator","dataset":"threatintel.abuseurl"},"matched":{"atomic":"192.168.1.19","field":"host.ip","id":"iiL9NHoB_Blp1Ib9yoJo","index":"filebeat-8.0.0-2021.05.28-000001","type":"url"}}`, + `{"first_seen":"2021-03-21T19:40:19.000Z","provider":"geenensp","ip":"192.168.1.19","type":"url","event":{"reference":"https://urlhaus.abuse.ch/url/1055419/","ingested":"2021-03-08T19:40:44.213673Z","created":"2021-03-08T19:40:43.160Z","kind":"other","module":"threatintel","category":"threat","type":"indicator","dataset":"threatintel.abuseurl"},"matched":{"atomic":"192.168.1.19","field":"host.ip","id":"0SIZMnoB_Blp1Ib9ZYHU","index":"filebeat-8.0.0-2021.05.28-000001","type":"url"}}`, + ], + }, + ]; + + expect(parseExistingEnrichments(data)).toEqual([ + expect.arrayContaining([ + { + category: 'first_seen', + field: 'first_seen', + isObjectArray: false, + originalValue: ['2021-03-21T19:40:19.000Z'], + values: ['2021-03-21T19:40:19.000Z'], + }, + { + category: 'provider', + field: 'provider', + isObjectArray: false, + originalValue: ['other'], + values: ['other'], + }, + { + category: 'ip', + field: 'ip', + isObjectArray: false, + originalValue: ['192.168.1.19'], + values: ['192.168.1.19'], + }, + { + category: 'type', + field: 'type', + isObjectArray: false, + originalValue: ['url'], + values: ['url'], + }, + { + category: 'event', + field: 'event.reference', + isObjectArray: false, + originalValue: ['https://urlhaus.abuse.ch/url/1055419/'], + values: ['https://urlhaus.abuse.ch/url/1055419/'], + }, + { + category: 'event', + field: 'event.ingested', + isObjectArray: false, + originalValue: ['2021-03-08T19:40:44.213673Z'], + values: ['2021-03-08T19:40:44.213673Z'], + }, + { + category: 'event', + field: 'event.module', + isObjectArray: false, + originalValue: ['threatintel'], + values: ['threatintel'], + }, + { + category: 'event', + field: 'event.category', + isObjectArray: false, + originalValue: ['threat'], + values: ['threat'], + }, + { + category: 'event', + field: 'event.type', + isObjectArray: false, + originalValue: ['indicator'], + values: ['indicator'], + }, + { + category: 'event', + field: 'event.dataset', + isObjectArray: false, + originalValue: ['threatintel.abuseurl'], + values: ['threatintel.abuseurl'], + }, + { + category: 'matched', + field: 'matched.atomic', + isObjectArray: false, + originalValue: ['192.168.1.19'], + values: ['192.168.1.19'], + }, + { + category: 'matched', + field: 'matched.field', + isObjectArray: false, + originalValue: ['host.ip'], + values: ['host.ip'], + }, + { + category: 'matched', + field: 'matched.id', + isObjectArray: false, + originalValue: ['iiL9NHoB_Blp1Ib9yoJo'], + values: ['iiL9NHoB_Blp1Ib9yoJo'], + }, + { + category: 'matched', + field: 'matched.index', + isObjectArray: false, + originalValue: ['filebeat-8.0.0-2021.05.28-000001'], + values: ['filebeat-8.0.0-2021.05.28-000001'], + }, + { + category: 'matched', + field: 'matched.type', + isObjectArray: false, + originalValue: ['url'], + values: ['url'], + }, + ]), + expect.arrayContaining([ + { + category: 'first_seen', + field: 'first_seen', + isObjectArray: false, + originalValue: ['2021-03-21T19:40:19.000Z'], + values: ['2021-03-21T19:40:19.000Z'], + }, + { + category: 'provider', + field: 'provider', + isObjectArray: false, + originalValue: ['geenensp'], + values: ['geenensp'], + }, + { + category: 'ip', + field: 'ip', + isObjectArray: false, + originalValue: ['192.168.1.19'], + values: ['192.168.1.19'], + }, + { + category: 'type', + field: 'type', + isObjectArray: false, + originalValue: ['url'], + values: ['url'], + }, + { + category: 'event', + field: 'event.reference', + isObjectArray: false, + originalValue: ['https://urlhaus.abuse.ch/url/1055419/'], + values: ['https://urlhaus.abuse.ch/url/1055419/'], + }, + { + category: 'event', + field: 'event.ingested', + isObjectArray: false, + originalValue: ['2021-03-08T19:40:44.213673Z'], + values: ['2021-03-08T19:40:44.213673Z'], + }, + { + category: 'event', + field: 'event.module', + isObjectArray: false, + originalValue: ['threatintel'], + values: ['threatintel'], + }, + { + category: 'event', + field: 'event.category', + isObjectArray: false, + originalValue: ['threat'], + values: ['threat'], + }, + { + category: 'event', + field: 'event.type', + isObjectArray: false, + originalValue: ['indicator'], + values: ['indicator'], + }, + { + category: 'event', + field: 'event.dataset', + isObjectArray: false, + originalValue: ['threatintel.abuseurl'], + values: ['threatintel.abuseurl'], + }, + { + category: 'matched', + field: 'matched.atomic', + isObjectArray: false, + originalValue: ['192.168.1.19'], + values: ['192.168.1.19'], + }, + { + category: 'matched', + field: 'matched.field', + isObjectArray: false, + originalValue: ['host.ip'], + values: ['host.ip'], + }, + { + category: 'matched', + field: 'matched.id', + isObjectArray: false, + originalValue: ['0SIZMnoB_Blp1Ib9ZYHU'], + values: ['0SIZMnoB_Blp1Ib9ZYHU'], + }, + { + category: 'matched', + field: 'matched.index', + isObjectArray: false, + originalValue: ['filebeat-8.0.0-2021.05.28-000001'], + values: ['filebeat-8.0.0-2021.05.28-000001'], + }, + { + category: 'matched', + field: 'matched.type', + isObjectArray: false, + originalValue: ['url'], + values: ['url'], + }, + ]), + ]); + }); +}); + +describe('filterDuplicateEnrichments', () => { + it('returns an empty array if given one', () => { + expect(filterDuplicateEnrichments([])).toEqual([]); + }); + + it('returns the existing enrichment if given both that and an investigation-time enrichment for the same indicator and field', () => { + const existingEnrichment = buildEventEnrichmentMock({ + 'matched.type': [ENRICHMENT_TYPES.IndicatorMatchRule], + }); + const investigationEnrichment = buildEventEnrichmentMock({ + 'matched.type': [ENRICHMENT_TYPES.InvestigationTime], + }); + expect(filterDuplicateEnrichments([existingEnrichment, investigationEnrichment])).toEqual([ + existingEnrichment, + ]); + }); + + it('includes two enrichments from the same indicator if it matched different fields', () => { + const enrichments = [ + buildEventEnrichmentMock(), + buildEventEnrichmentMock({ + 'matched.field': ['other.field'], + }), + ]; + expect(filterDuplicateEnrichments(enrichments)).toEqual(enrichments); + }); +}); + +describe('getEnrichmentFields', () => { + it('returns an empty object if items is empty', () => { + expect(getEnrichmentFields([])).toEqual({}); + }); + + it('returns an object of event fields and values', () => { + const data = [ + { + category: 'source', + field: 'source.ip', + isObjectArray: false, + originalValue: ['192.168.1.1'], + values: ['192.168.1.1'], + }, + { + category: 'event', + field: 'event.reference', + isObjectArray: false, + originalValue: ['https://urlhaus.abuse.ch/url/1055419/'], + values: ['https://urlhaus.abuse.ch/url/1055419/'], + }, + ]; + expect(getEnrichmentFields(data)).toEqual({ + 'source.ip': '192.168.1.1', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx new file mode 100644 index 00000000000000..b048bb076e2d37 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx @@ -0,0 +1,123 @@ +/* + * 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 { groupBy } from 'lodash'; +import { + DEFAULT_INDICATOR_SOURCE_PATH, + INDICATOR_DESTINATION_PATH, +} from '../../../../../common/constants'; +import { + ENRICHMENT_TYPES, + MATCHED_ATOMIC, + MATCHED_FIELD, + MATCHED_ID, + MATCHED_TYPE, + PROVIDER, +} from '../../../../../common/cti/constants'; +import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; +import { + CtiEnrichment, + CtiEnrichmentIdentifiers, + EventFields, + isValidEventField, +} from '../../../../../common/search_strategy/security_solution/cti'; +import { getFirstElement } from '../../../../../common/utils/data_retrieval'; +import { getDataFromSourceHits } from '../../../../../common/utils/field_formatters'; + +export const isInvestigationTimeEnrichment = (type: string | undefined) => + type === ENRICHMENT_TYPES.InvestigationTime; + +export const parseExistingEnrichments = ( + data: TimelineEventsDetailsItem[] +): TimelineEventsDetailsItem[][] => { + const threatIndicatorField = data.find( + ({ field, originalValue }) => field === INDICATOR_DESTINATION_PATH && originalValue + ); + if (!threatIndicatorField) { + return []; + } + + const { originalValue } = threatIndicatorField; + const enrichmentStrings = Array.isArray(originalValue) ? originalValue : [originalValue]; + + return enrichmentStrings.reduce( + (enrichments, enrichmentString) => { + try { + const enrichment = getDataFromSourceHits(JSON.parse(enrichmentString)); + enrichments.push(enrichment); + } catch (e) { + // omit failed parse + } + return enrichments; + }, + [] + ); +}; + +export const timelineDataToEnrichment = (data: TimelineEventsDetailsItem[]): CtiEnrichment => + data.reduce((acc, item) => { + acc[item.field] = item.originalValue; + return acc; + }, {}); + +export const getEnrichmentValue = (enrichment: CtiEnrichment, field: string) => + getFirstElement(enrichment[field]) as string | undefined; + +/** + * These fields (e.g. 'x') may be in one of two keys depending on whether it's + * a new enrichment ('threatintel.indicator.x') or an old indicator alert + * (simply 'x'). Once enrichment has been normalized and we support the new ECS + * fields, this value should always be 'indicator.x'; + */ +export const getShimmedIndicatorValue = (enrichment: CtiEnrichment, field: string) => + getEnrichmentValue(enrichment, field) || + getEnrichmentValue(enrichment, `${DEFAULT_INDICATOR_SOURCE_PATH}.${field}`); + +export const getEnrichmentIdentifiers = (enrichment: CtiEnrichment): CtiEnrichmentIdentifiers => ({ + id: getEnrichmentValue(enrichment, MATCHED_ID), + field: getEnrichmentValue(enrichment, MATCHED_FIELD), + value: getEnrichmentValue(enrichment, MATCHED_ATOMIC), + type: getEnrichmentValue(enrichment, MATCHED_TYPE), + provider: getShimmedIndicatorValue(enrichment, PROVIDER), +}); + +const buildEnrichmentId = (enrichment: CtiEnrichment): string => { + const { id, field } = getEnrichmentIdentifiers(enrichment); + return `${id}${field}`; +}; + +/** + * This function receives an array of enrichments and removes + * investigation-time enrichments if that exact indicator already exists + * elsewhere in the list. + * + * @param enrichments {@type CtiEnrichment[]} + */ +export const filterDuplicateEnrichments = (enrichments: CtiEnrichment[]): CtiEnrichment[] => { + if (enrichments.length < 2) { + return enrichments; + } + const enrichmentsById = groupBy(enrichments, buildEnrichmentId); + + return Object.values(enrichmentsById).map( + (enrichmentGroup) => + enrichmentGroup.find( + (enrichment) => !isInvestigationTimeEnrichment(getEnrichmentValue(enrichment, MATCHED_TYPE)) + ) ?? enrichmentGroup[0] + ); +}; + +export const getEnrichmentFields = (items: TimelineEventsDetailsItem[]): EventFields => + items.reduce((fields, item) => { + if (isValidEventField(item.field)) { + const value = getFirstElement(item.originalValue); + if (value) { + return { ...fields, [item.field]: value }; + } + } + return fields; + }, {}); 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 new file mode 100644 index 00000000000000..0113dde96a4b6c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.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 React from 'react'; +import { mount } from 'enzyme'; + +import { TestProviders } from '../../../mock'; +import { buildEventEnrichmentMock } from '../../../../../common/search_strategy/security_solution/cti/index.mock'; +import { FIRSTSEEN } from '../../../../../common/cti/constants'; +import { ThreatDetailsView } from './threat_details_view'; + +describe('ThreatDetailsView', () => { + it('renders a detail view for each enrichment', () => { + const enrichments = [ + buildEventEnrichmentMock(), + buildEventEnrichmentMock({ 'matched.id': ['other.id'], 'matched.field': ['other.field'] }), + ]; + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj^="threat-details-view"]').hostNodes()).toHaveLength( + enrichments.length + ); + }); + + 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({ + 'event.url': ['http://foo.bar'], + 'event.reference': ['http://foo.baz'], + }), + ]; + const wrapper = mount( + + + + ); + expect(wrapper.find('a').length).toEqual(2); + }); + + it('orders enrichments by first_seen descending', () => { + const mostRecentDate = '2021-04-25T18:17:00.000Z'; + const olderDate = '2021-03-25T18:17:00.000Z'; + // this simulates a legacy enrichment from the old indicator match rule, + // where first_seen is available at the top level + const existingEnrichment = buildEventEnrichmentMock({ + first_seen: [mostRecentDate], + }); + delete existingEnrichment['threatintel.indicator.first_seen']; + const newEnrichment = buildEventEnrichmentMock({ + 'matched.id': ['other.id'], + 'threatintel.indicator.first_seen': [olderDate], + }); + const enrichments = [existingEnrichment, newEnrichment]; + + const wrapper = mount( + + + + ); + + const firstSeenRows = wrapper + .find('.euiTableRow') + .hostNodes() + .filterWhere((node) => node.text().includes(FIRSTSEEN)); + expect(firstSeenRows.map((node) => node.text())).toEqual([ + `first_seen${mostRecentDate}`, + `first_seen${olderDate}`, + ]); + }); +}); 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 new file mode 100644 index 00000000000000..d5e985c5757a62 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx @@ -0,0 +1,163 @@ +/* + * 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 { + EuiBasicTableColumn, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiSpacer, + EuiToolTip, + EuiLink, + EuiText, + EuiTextColor, +} from '@elastic/eui'; +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'; +import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; +import { + getShimmedIndicatorValue, + isInvestigationTimeEnrichment, + getEnrichmentIdentifiers, +} from './helpers'; +import * as i18n from './translations'; +import { EnrichmentIcon } from './enrichment_icon'; +import { QUERY_ID } from '../../../containers/cti/event_enrichment/use_investigation_enrichment'; +import { InspectButton } from '../../inspect'; + +const getFirstSeen = (enrichment: CtiEnrichment): number => { + const firstSeenValue = getShimmedIndicatorValue(enrichment, FIRSTSEEN); + const firstSeenDate = Date.parse(firstSeenValue ?? 'no date'); + return Number.isInteger(firstSeenDate) ? firstSeenDate : new Date(-1).valueOf(); +}; + +const ThreatDetailsHeader: React.FC<{ + field: string | undefined; + value: string | undefined; + provider: string | undefined; + type: string | undefined; +}> = ({ field, value, provider, type }) => ( + <> + + + + + + + + {field} {value} + + + {provider && ( + <> + + + {i18n.PROVIDER_PREPOSITION} {provider} + + + + )} + + + + + + {isInvestigationTimeEnrichment(type) && ( + + + + + + )} + +); + +const ThreatDetailsDescription: React.FC = ({ + fieldName, + value, +}) => { + const tooltipChild = [EVENT_URL, EVENT_REFERENCE].includes(fieldName) ? ( + + {value} + + ) : ( + {value} + ); + return ( + + + {fieldName} + + + } + > + {tooltipChild} + + ); +}; + +const columns: Array> = getSummaryColumns(ThreatDetailsDescription); + +const buildThreatDetailsItems = (enrichment: CtiEnrichment) => + Object.keys(enrichment) + .sort() + .map((field) => { + const displayField = field.startsWith(DEFAULT_INDICATOR_SOURCE_PATH) + ? field.replace(`${DEFAULT_INDICATOR_SOURCE_PATH}.`, '') + : field; + + return { + title: displayField, + description: { + fieldName: field, + value: getFirstElement(enrichment[field]), + }, + }; + }); + +const ThreatDetailsViewComponent: React.FC<{ + enrichments: CtiEnrichment[]; +}> = ({ enrichments }) => { + if (enrichments.length < 1) { + return ; + } + + const sortedEnrichments = enrichments.sort((a, b) => getFirstSeen(b) - getFirstSeen(a)); + + return ( + <> + + {sortedEnrichments.map((enrichment, index) => { + const { id, field, provider, type, value } = getEnrichmentIdentifiers(enrichment); + + return ( + + + + {index < sortedEnrichments.length - 1 && } + + ); + })} + + ); +}; + +export const ThreatDetailsView = React.memo(ThreatDetailsViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx new file mode 100644 index 00000000000000..bf6c4b9594344a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { ThreatSummaryView } from './threat_summary_view'; +import { TestProviders } from '../../../mock'; +import { useMountAppended } from '../../../utils/use_mount_appended'; +import { buildEventEnrichmentMock } from '../../../../../common/search_strategy/security_solution/cti/index.mock'; + +jest.mock('../../../../timelines/components/timeline/body/renderers/formatted_field'); + +describe('ThreatSummaryView', () => { + const mount = useMountAppended(); + const eventId = '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31'; + const timelineId = 'detections-page'; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders a row for each enrichment', () => { + const enrichments = [ + buildEventEnrichmentMock(), + buildEventEnrichmentMock({ 'matched.id': ['other.id'], 'matched.field': ['other.field'] }), + ]; + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="threat-summary-view"] .euiTableRow')).toHaveLength( + enrichments.length + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx new file mode 100644 index 00000000000000..4a6c9ec48bcbc7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import styled from 'styled-components'; +import React from 'react'; +import { EuiBasicTableColumn, EuiText, EuiTitle } from '@elastic/eui'; + +import * as i18n from './translations'; +import { StyledEuiInMemoryTable } from '../summary_view'; +import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; +import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; +import { getEnrichmentIdentifiers } from './helpers'; +import { EnrichmentIcon } from './enrichment_icon'; + +export interface ThreatSummaryItem { + title: { + title: string | undefined; + type: string | undefined; + }; + description: { + timelineId: string; + eventId: string; + fieldName: string | undefined; + index: number; + value: string | undefined; + provider: string | undefined; + }; +} + +const RightMargin = styled.span` + margin-right: ${({ theme }) => theme.eui.paddingSizes.s}; +`; + +const EnrichmentTitle: React.FC = ({ title, type }) => ( + <> + + + + +
{title}
+
+ +); + +const EnrichmentDescription: React.FC = ({ + timelineId, + eventId, + fieldName, + index, + value, + provider, +}) => { + const key = `alert-details-value-formatted-field-value-${timelineId}-${eventId}-${fieldName}-${value}-${index}-${provider}`; + return ( + <> + + + + {provider && ( + <> + + + {i18n.PROVIDER_PREPOSITION} + + + + + {provider} + + + + )} + + ); +}; + +const buildThreatSummaryItems = ( + enrichments: CtiEnrichment[], + timelineId: string, + eventId: string +) => { + return enrichments.map((enrichment, index) => { + const { field, type, value, provider } = getEnrichmentIdentifiers(enrichment); + + return { + title: { + title: field, + type, + }, + description: { + eventId, + fieldName: field, + index, + provider, + timelineId, + value, + }, + }; + }); +}; + +const columns: Array> = [ + { + field: 'title', + truncateText: false, + render: EnrichmentTitle, + width: '160px', + name: '', + }, + { + field: 'description', + truncateText: false, + render: EnrichmentDescription, + name: '', + }, +]; + +const ThreatSummaryViewComponent: React.FC<{ + enrichments: CtiEnrichment[]; + timelineId: string; + eventId: string; +}> = ({ enrichments, timelineId, eventId }) => ( + +); + +export const ThreatSummaryView = React.memo(ThreatSummaryViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts new file mode 100644 index 00000000000000..a0c247db927ce6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts @@ -0,0 +1,71 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const PROVIDER_PREPOSITION = i18n.translate( + 'xpack.securitySolution.eventDetails.ctiSummary.providerPreposition', + { + defaultMessage: 'from', + } +); + +export const INDICATOR_TOOLTIP_TITLE = i18n.translate( + 'xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipTitle', + { + defaultMessage: 'Indicator rule enrichment', + } +); + +export const INVESTIGATION_TOOLTIP_TITLE = i18n.translate( + 'xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipTitle', + { + defaultMessage: 'Investigation time enrichment', + } +); + +export const INDICATOR_TOOLTIP_CONTENT = i18n.translate( + 'xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipContent', + { + defaultMessage: + 'This field matched a known indicator, and was enriched by an indicator match rule. See more details on the Threat Intel tab.', + } +); + +export const INVESTIGATION_TOOLTIP_CONTENT = i18n.translate( + 'xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent', + { + defaultMessage: + 'This field matched a known indicator; see more details on the Threat Intel tab.', + } +); + +export const NO_ENRICHMENT_FOUND = i18n.translate( + 'xpack.securitySolution.alertDetails.noEnrichmentFound', + { + defaultMessage: 'No Threat Intel Enrichment Found', + } +); + +export const IF_CTI_NOT_ENABLED = i18n.translate( + 'xpack.securitySolution.alertDetails.ifCtiNotEnabled', + { + defaultMessage: + "If you haven't enabled any threat intelligence sources and want to learn more about this capability, ", + } +); + +export const CHECK_DOCS = i18n.translate('xpack.securitySolution.alertDetails.checkDocs', { + defaultMessage: 'please check out our documentation.', +}); + +export const INVESTIGATION_QUERY_TITLE = i18n.translate( + 'xpack.securitySolution.alertDetails.investigationTimeQueryTitle', + { + defaultMessage: 'Investigation time enrichment', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index 6aff259d8220e2..f599cfa242dea6 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -19,8 +19,10 @@ import { useMountAppended } from '../../utils/use_mount_appended'; import { mockAlertDetailsData } from './__mocks__'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { TimelineTabs } from '../../../../common/types/timeline'; +import { useInvestigationTimeEnrichment } from '../../containers/cti/event_enrichment'; jest.mock('../../../common/lib/kibana'); +jest.mock('../../containers/cti/event_enrichment'); jest.mock('../link_to'); describe('EventDetails', () => { @@ -46,6 +48,7 @@ describe('EventDetails', () => { let wrapper: ReactWrapper; let alertsWrapper: ReactWrapper; beforeAll(async () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({}); wrapper = mount( 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 c4092214633e5f..d07cdd81aa5f45 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 @@ -5,27 +5,37 @@ * 2.0. */ -import { EuiTabbedContent, EuiTabbedContentTab, EuiSpacer } from '@elastic/eui'; +import { + EuiTabbedContent, + EuiTabbedContentTab, + EuiSpacer, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingContent, + EuiLoadingSpinner, +} from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import { EventFieldsBrowser } from './event_fields_browser'; import { JsonView } from './json_view'; -import { ThreatSummaryView } from './threat_summary_view'; -import { ThreatDetailsView } from './threat_details_view'; +import { ThreatSummaryView } from './cti_details/threat_summary_view'; +import { ThreatDetailsView } from './cti_details/threat_details_view'; import * as i18n from './translations'; import { AlertSummaryView } from './alert_summary_view'; import { BrowserFields } from '../../containers/source'; +import { useInvestigationTimeEnrichment } from '../../containers/cti/event_enrichment'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; import { TimelineTabs } from '../../../../common/types/timeline'; -import { INDICATOR_DESTINATION_PATH } from '../../../../common/constants'; -import { getDataFromSourceHits } from '../../../../common/utils/field_formatters'; +import { + filterDuplicateEnrichments, + getEnrichmentFields, + parseExistingEnrichments, + timelineDataToEnrichment, +} from './cti_details/helpers'; -interface EventViewTab { - id: EventViewId; - name: string; - content: JSX.Element; -} +type EventViewTab = EuiTabbedContentTab; export type EventViewId = | EventsViewType.tableView @@ -90,23 +100,33 @@ const EventDetailsComponent: React.FC = ({ (tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as EventViewId), [setSelectedTabId] ); + const viewThreatIntelTab = useCallback(() => setSelectedTabId(EventsViewType.threatIntelView), [ + setSelectedTabId, + ]); - const threatData = useMemo(() => { - if (isAlert && data) { - const threatIndicator = data.find( - ({ field, originalValue }) => field === INDICATOR_DESTINATION_PATH && originalValue - ); - if (!threatIndicator) return []; - const { originalValue } = threatIndicator; - const values = Array.isArray(originalValue) ? originalValue : [originalValue]; - return values.map((value) => getDataFromSourceHits(JSON.parse(value))); + const eventFields = useMemo(() => getEnrichmentFields(data ?? []), [data]); + const existingEnrichments = useMemo( + () => + isAlert + ? parseExistingEnrichments(data).map((enrichmentData) => + timelineDataToEnrichment(enrichmentData) + ) + : [], + [data, isAlert] + ); + const { + loading: enrichmentsLoading, + result: enrichmentsResponse, + } = useInvestigationTimeEnrichment(eventFields); + const allEnrichments = useMemo(() => { + if (enrichmentsLoading || !enrichmentsResponse?.enrichments) { + return existingEnrichments; } - return []; - }, [data, isAlert]); - - const threatCount = useMemo(() => threatData.length, [threatData.length]); + return filterDuplicateEnrichments([...existingEnrichments, ...enrichmentsResponse.enrichments]); + }, [enrichmentsLoading, enrichmentsResponse, existingEnrichments]); + const enrichmentCount = allEnrichments.length; - const summaryTab = useMemo( + const summaryTab: EventViewTab | undefined = useMemo( () => isAlert ? { @@ -120,15 +140,44 @@ const EventDetailsComponent: React.FC = ({ eventId: id, browserFields, timelineId, - title: threatCount ? i18n.ALERT_SUMMARY : undefined, + title: i18n.ALERT_SUMMARY, }} /> - {threatCount > 0 && } + {enrichmentsLoading && ( + <> + + + )} + {enrichmentCount > 0 && ( + <> + + + + + {i18n.VIEW_CTI_DATA} + + + + )} ), } : undefined, - [browserFields, data, id, isAlert, timelineId, threatCount] + [ + isAlert, + data, + id, + browserFields, + timelineId, + enrichmentsLoading, + enrichmentCount, + allEnrichments, + viewThreatIntelTab, + ] ); const threatIntelTab = useMemo( @@ -137,11 +186,16 @@ const EventDetailsComponent: React.FC = ({ ? { id: EventsViewType.threatIntelView, 'data-test-subj': 'threatIntelTab', - name: `${i18n.THREAT_INTEL} (${threatCount})`, - content: , + name: ( + + {`${i18n.THREAT_INTEL} `} + {enrichmentsLoading ? : `(${enrichmentCount})`} + + ), + content: , } : undefined, - [isAlert, threatCount, threatData] + [allEnrichments, enrichmentCount, enrichmentsLoading, isAlert] ); const tableTab = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx index 8392be420a2c53..6002f66da43092 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx @@ -64,16 +64,6 @@ export interface AlertSummaryRow { }; } -export interface ThreatSummaryRow { - title: string; - description: { - contextId: string; - eventId: string; - fieldName: string; - values: string[]; - }; -} - export interface ThreatDetailsRow { title: string; description: { @@ -82,7 +72,7 @@ export interface ThreatDetailsRow { }; } -export type SummaryRow = AlertSummaryRow | ThreatSummaryRow | ThreatDetailsRow; +export type SummaryRow = AlertSummaryRow | ThreatDetailsRow; export const getColumnHeaderFromBrowserField = ({ browserField, @@ -215,7 +205,6 @@ getTitle.displayName = 'getTitle'; export const getSummaryColumns = ( DescriptionComponent: - | React.FC | React.FC | React.FC ): Array> => { 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 1dda40ae4b19d7..0e846f3f6f6998 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 @@ -12,19 +12,13 @@ import styled from 'styled-components'; import { SummaryRow } from './helpers'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` +export const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` .euiTableHeaderCell { border: none; } .euiTableRowCell { border: none; } - - .euiTableCellContent { - display: flex; - flex-direction: column; - align-items: flex-start; - } `; const StyledEuiTitle = styled(EuiTitle)` diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.test.tsx deleted file mode 100644 index 4b2f56a2050421..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.test.tsx +++ /dev/null @@ -1,114 +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 React from 'react'; - -import { ThreatDetailsView } from './threat_details_view'; - -import { TestProviders } from '../../mock'; -import { useMountAppended } from '../../utils/use_mount_appended'; - -jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => { - return { - useRuleAsync: jest.fn(), - }; -}); - -const mostRecentDate = '2021-04-25T18:17:00.000Z'; - -const threatData = [ - [ - { - category: 'matched', - field: 'matched.field', - isObjectArray: false, - originalValue: ['test_field_2'], - values: ['test_field_2'], - }, - { - category: 'first_seen', - field: 'first_seen', - isObjectArray: false, - originalValue: ['2019-04-25T18:17:00.000Z'], - values: ['2019-04-25T18:17:00.000Z'], - }, - { - category: 'event', - field: 'event.reference', - isObjectArray: false, - originalValue: ['https://test.com/'], - values: ['https://test.com/'], - }, - { - category: 'event', - field: 'event.url', - isObjectArray: false, - originalValue: ['https://test2.com/'], - values: ['https://test2.com/'], - }, - ], - [ - { - category: 'first_seen', - field: 'first_seen', - isObjectArray: false, - originalValue: [mostRecentDate], - values: [mostRecentDate], - }, - { - category: 'matched', - field: 'matched.field', - isObjectArray: false, - originalValue: ['test_field'], - values: ['test_field'], - }, - ], -]; - -describe('ThreatDetailsView', () => { - const mount = useMountAppended(); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('render correct items', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="threat-details-view-0"]').exists()).toEqual(true); - }); - - test('renders empty view if there are no items', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="empty-threat-details-view"]').exists()).toEqual(true); - }); - - test('renders link for event.url and event.reference', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('a').length).toEqual(2); - }); - - test('orders items by first_seen', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('.euiToolTipAnchor span').at(0).text()).toEqual(mostRecentDate); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.tsx deleted file mode 100644 index 0f577200b7b47b..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.tsx +++ /dev/null @@ -1,122 +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 { - EuiBasicTableColumn, - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiSpacer, - EuiToolTip, - EuiLink, -} from '@elastic/eui'; -import React from 'react'; - -import { isEmpty } from 'fp-ts/Array'; -import { SummaryView } from './summary_view'; -import { getSummaryColumns, SummaryRow, ThreatDetailsRow } from './helpers'; -import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; -import { INDICATOR_DESTINATION_PATH } from '../../../../common/constants'; -import { - FIRSTSEEN, - INDICATOR_EVENT_URL, - INDICATOR_REFERENCE, -} from '../../../../common/cti/constants'; -import { EmptyThreatDetailsView } from './empty_threat_details_view'; - -const ThreatDetailsDescription: React.FC = ({ - fieldName, - value, -}) => { - const tooltipChild = [INDICATOR_EVENT_URL, INDICATOR_REFERENCE].some( - (field) => field === fieldName - ) ? ( - - {value} - - ) : ( - {value} - ); - return ( - - - {fieldName} - - - } - > - {tooltipChild} - - ); -}; - -const summaryColumns: Array> = getSummaryColumns( - ThreatDetailsDescription -); - -const getISOStringFromThreatDataItem = (threatDataItem: TimelineEventsDetailsItem[]) => { - const firstSeen = threatDataItem.find( - (item: TimelineEventsDetailsItem) => item.field === FIRSTSEEN - ); - if (firstSeen) { - const { originalValue } = firstSeen; - const firstSeenValue = Array.isArray(originalValue) ? originalValue[0] : originalValue; - if (!Number.isNaN(Date.parse(firstSeenValue))) { - return firstSeenValue; - } - } - return new Date(-1).toString(); -}; - -const getThreatDetailsRowsArray = (threatData: TimelineEventsDetailsItem[][]) => - threatData - .sort( - (a, b) => - Date.parse(getISOStringFromThreatDataItem(b)) - - Date.parse(getISOStringFromThreatDataItem(a)) - ) - .map((items) => - items.map(({ field, originalValue }) => ({ - title: field, - description: { - fieldName: `${INDICATOR_DESTINATION_PATH}.${field}`, - value: Array.isArray(originalValue) ? originalValue[0] : originalValue, - }, - })) - ); - -const ThreatDetailsViewComponent: React.FC<{ - threatData: TimelineEventsDetailsItem[][]; -}> = ({ threatData }) => { - const threatDetailsRowsArray = getThreatDetailsRowsArray(threatData); - return isEmpty(threatDetailsRowsArray) || isEmpty(threatDetailsRowsArray[0]) ? ( - - ) : ( - <> - {threatDetailsRowsArray.map((summaryRows, index, arr) => { - const key = summaryRows.find((threat) => threat.title === 'matched.id')?.description - .value[0]; - return ( -
- {index === 0 && } - - {index < arr.length - 1 && } -
- ); - })} - - ); -}; - -export const ThreatDetailsView = React.memo(ThreatDetailsViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.test.tsx deleted file mode 100644 index fa12ff3db7895c..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { ThreatSummaryView } from './threat_summary_view'; -import { TestProviders } from '../../mock'; -import { useMountAppended } from '../../utils/use_mount_appended'; -import { mockAlertDetailsData } from './__mocks__'; -import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; - -jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => { - return { - useRuleAsync: jest.fn(), - }; -}); - -const props = { - data: mockAlertDetailsData as TimelineEventsDetailsItem[], - eventId: '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31', - timelineId: 'detections-page', -}; - -describe('ThreatSummaryView', () => { - const mount = useMountAppended(); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('render correct items', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="threat-summary-view"]').exists()).toEqual(true); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx deleted file mode 100644 index 67b09e8e596990..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx +++ /dev/null @@ -1,78 +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 { EuiBasicTableColumn, EuiSpacer } from '@elastic/eui'; -import React from 'react'; - -import * as i18n from './translations'; -import { SummaryView } from './summary_view'; -import { getSummaryColumns, SummaryRow, ThreatSummaryRow } from './helpers'; -import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field'; -import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; -import { SORTED_THREAT_SUMMARY_FIELDS } from '../../../../common/cti/constants'; -import { INDICATOR_DESTINATION_PATH } from '../../../../common/constants'; - -const getThreatSummaryRows = ( - data: TimelineEventsDetailsItem[], - timelineId: string, - eventId: string -) => - SORTED_THREAT_SUMMARY_FIELDS.map((threatSummaryField) => { - const item = data.find(({ field }) => field === threatSummaryField); - if (item) { - const { field, originalValue } = item; - return { - title: field.replace(`${INDICATOR_DESTINATION_PATH}.`, ''), - description: { - values: Array.isArray(originalValue) ? originalValue : [originalValue], - contextId: timelineId, - eventId, - fieldName: field, - }, - }; - } - return null; - }).filter((item: ThreatSummaryRow | null): item is ThreatSummaryRow => !!item); - -const getDescription = ({ - contextId, - eventId, - fieldName, - values, -}: ThreatSummaryRow['description']): JSX.Element => ( - <> - {values.map((value: string) => ( - - ))} - -); - -const summaryColumns: Array> = getSummaryColumns(getDescription); - -const ThreatSummaryViewComponent: React.FC<{ - data: TimelineEventsDetailsItem[]; - timelineId: string; - eventId: string; -}> = ({ data, timelineId, eventId }) => ( - <> - - - -); - -export const ThreatSummaryView = React.memo(ThreatSummaryViewComponent); 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 a28d1976ca9400..a17ca5e434ace7 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 @@ -23,23 +23,8 @@ export const THREAT_SUMMARY = i18n.translate('xpack.securitySolution.alertDetail defaultMessage: 'Threat Summary', }); -export const NO_ENRICHMENT_FOUND = i18n.translate( - 'xpack.securitySolution.alertDetails.noEnrichmentFound', - { - defaultMessage: 'No Threat Intel Enrichment Found', - } -); - -export const IF_CTI_NOT_ENABLED = i18n.translate( - 'xpack.securitySolution.alertDetails.ifCtiNotEnabled', - { - defaultMessage: - "If you haven't enabled any threat intelligence sources and want to learn more about this capability, ", - } -); - -export const CHECK_DOCS = i18n.translate('xpack.securitySolution.alertDetails.checkDocs', { - defaultMessage: 'please check out our documentation.', +export const VIEW_CTI_DATA = i18n.translate('xpack.securitySolution.alertDetails.threatIntelCta', { + defaultMessage: 'View threat intel data', }); export const INVESTIGATION_GUIDE = i18n.translate( diff --git a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/api.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/api.ts new file mode 100644 index 00000000000000..179b4a53e3676c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/api.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 { Observable } from 'rxjs'; +import { filter } from 'rxjs/operators'; + +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { + isErrorResponse, + isCompleteResponse, +} from '../../../../../../../../src/plugins/data/common'; +import { + CtiEventEnrichmentRequestOptions, + CtiEventEnrichmentStrategyResponse, + CtiQueries, +} from '../../../../../common/search_strategy/security_solution/cti'; + +type GetEventEnrichmentProps = CtiEventEnrichmentRequestOptions & { + data: DataPublicPluginStart; + signal: AbortSignal; +}; + +export const getEventEnrichment = ({ + data, + defaultIndex, + eventFields, + filterQuery, + timerange, + signal, +}: GetEventEnrichmentProps): Observable => + data.search.search( + { + defaultIndex, + eventFields, + factoryQueryType: CtiQueries.eventEnrichment, + filterQuery, + timerange, + }, + { + strategy: 'securitySolutionSearchStrategy', + abortSignal: signal, + } + ); + +export const getEventEnrichmentComplete = ( + props: GetEventEnrichmentProps +): Observable => + getEventEnrichment(props).pipe( + filter((response) => isErrorResponse(response) || isCompleteResponse(response)) + ); diff --git a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/index.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/index.ts new file mode 100644 index 00000000000000..e8fb1a03045d9e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './use_event_enrichment'; +export * from './use_investigation_enrichment'; diff --git a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/translations.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/translations.ts new file mode 100644 index 00000000000000..ff9130b288fa89 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const INVESTIGATION_ENRICHMENT_REQUEST_ERROR = i18n.translate( + 'xpack.securitySolution.investigationEnrichment.requestError', + { + defaultMessage: `An error occurred while requesting threat intelligence`, + } +); diff --git a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_event_enrichment.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_event_enrichment.ts new file mode 100644 index 00000000000000..939566d6e59c32 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_event_enrichment.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useObservable, withOptionalSignal } from '@kbn/securitysolution-hook-utils'; + +import { getEventEnrichment, getEventEnrichmentComplete } from './api'; + +const getEventEnrichmentOptionalSignal = withOptionalSignal(getEventEnrichment); + +export const useEventEnrichment = () => useObservable(getEventEnrichmentOptionalSignal); + +const getEventEnrichmentCompleteWithOptionalSignal = withOptionalSignal(getEventEnrichmentComplete); + +export const useEventEnrichmentComplete = () => + useObservable(getEventEnrichmentCompleteWithOptionalSignal); diff --git a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts new file mode 100644 index 00000000000000..c15b49fe5c41e3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { isEmpty } from 'lodash'; + +import { EventFields } from '../../../../../common/search_strategy/security_solution/cti'; +import { + DEFAULT_CTI_SOURCE_INDEX, + DEFAULT_EVENT_ENRICHMENT_FROM, + DEFAULT_EVENT_ENRICHMENT_TO, +} from '../../../../../common/cti/constants'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useKibana } from '../../../lib/kibana'; +import { inputsActions } from '../../../store/actions'; +import * as i18n from './translations'; +import { useEventEnrichmentComplete } from '.'; + +export const QUERY_ID = 'investigation_time_enrichment'; +const noop = () => {}; + +export const useInvestigationTimeEnrichment = (eventFields: EventFields) => { + const { addError } = useAppToasts(); + const kibana = useKibana(); + const dispatch = useDispatch(); + const [{ from, to }, setRange] = useState({ + from: DEFAULT_EVENT_ENRICHMENT_FROM, + to: DEFAULT_EVENT_ENRICHMENT_TO, + }); + const { error, loading, result, start } = useEventEnrichmentComplete(); + + const deleteQuery = useCallback(() => { + dispatch(inputsActions.deleteOneQuery({ inputId: 'global', id: QUERY_ID })); + }, [dispatch]); + + useEffect(() => { + if (!loading && result) { + dispatch( + inputsActions.setQuery({ + inputId: 'global', + id: QUERY_ID, + inspect: { + dsl: result.inspect.dsl, + response: [JSON.stringify(result.rawResponse, null, 2)], + }, + loading, + refetch: noop, + }) + ); + } + + return deleteQuery; + }, [deleteQuery, dispatch, loading, result]); + + useEffect(() => { + if (error) { + addError(error, { title: i18n.INVESTIGATION_ENRICHMENT_REQUEST_ERROR }); + } + }, [addError, error]); + + useEffect(() => { + if (!isEmpty(eventFields)) { + start({ + data: kibana.services.data, + timerange: { from, to, interval: '' }, + defaultIndex: DEFAULT_CTI_SOURCE_INDEX, + eventFields, + filterQuery: '', + }); + } + }, [from, start, kibana.services.data, to, eventFields]); + + return { + loading, + result, + setRange, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts index 7c80572f6b1ee7..4a51880e0f227d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts @@ -7,6 +7,7 @@ import { get } from 'lodash'; import { INDICATOR_DESTINATION_PATH } from '../../../../../common/constants'; +import { ENRICHMENT_TYPES } from '../../../../../common/cti/constants'; import { getThreatListItemMock } from './build_threat_mapping_filter.mock'; import { @@ -158,14 +159,14 @@ describe('buildMatchedIndicator', () => { expect(get(indicator, 'matched.field')).toEqual('event.field'); }); - it('returns the type of the matched indicator as matched.type', () => { + it('returns the type of the enrichment as an indicator match type', () => { const [indicator] = buildMatchedIndicator({ queries, threats, indicatorPath, }); - expect(get(indicator, 'matched.type')).toEqual('type_1'); + expect(get(indicator, 'matched.type')).toEqual(ENRICHMENT_TYPES.IndicatorMatchRule); }); it('returns indicators for each provided query', () => { @@ -216,7 +217,7 @@ describe('buildMatchedIndicator', () => { id: '123', index: 'threat-index', field: 'event.field', - type: 'type_1', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, other: 'other_1', type: 'type_1', @@ -263,7 +264,7 @@ describe('buildMatchedIndicator', () => { id: '123', index: 'threat-index', field: 'event.field', - type: 'indicator_type', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, type: 'indicator_type', event: { @@ -294,7 +295,7 @@ describe('buildMatchedIndicator', () => { id: '123', index: 'threat-index', field: 'event.field', - type: undefined, + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, }, ]); @@ -321,7 +322,7 @@ describe('buildMatchedIndicator', () => { id: '123', index: 'threat-index', field: 'event.field', - type: undefined, + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, }, ]); @@ -359,7 +360,7 @@ describe('buildMatchedIndicator', () => { id: '123', index: 'threat-index', field: 'event.field', - type: 'first', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, type: 'first', event: { @@ -478,7 +479,7 @@ describe('enrichSignalThreatMatches', () => { id: '123', index: 'indicator_index', field: 'event.field', - type: 'type_1', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, other: 'other_1', type: 'type_1', @@ -510,7 +511,7 @@ describe('enrichSignalThreatMatches', () => { id: '123', index: 'indicator_index', field: 'event.field', - type: undefined, + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, }, ]); @@ -543,7 +544,7 @@ describe('enrichSignalThreatMatches', () => { id: '123', index: 'indicator_index', field: 'event.field', - type: 'type_1', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, other: 'other_1', type: 'type_1', @@ -608,7 +609,7 @@ describe('enrichSignalThreatMatches', () => { id: '123', index: 'custom_index', field: 'event.field', - type: 'custom_type', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, other: 'custom_other', type: 'custom_type', @@ -670,7 +671,7 @@ describe('enrichSignalThreatMatches', () => { id: '123', index: 'indicator_index', field: 'event.field', - type: 'type_1', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, event: { category: 'threat', @@ -685,7 +686,7 @@ describe('enrichSignalThreatMatches', () => { id: '456', index: 'other_custom_index', field: 'event.other', - type: 'type_2', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, event: { category: 'bad', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts index c26f03d1dd480b..3423cc1a8744f4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts @@ -6,6 +6,7 @@ */ import { get, isObject } from 'lodash'; +import { ENRICHMENT_TYPES } from '../../../../../common/cti/constants'; import type { SignalSearchResponse, SignalSourceHit } from '../types'; import type { @@ -56,13 +57,18 @@ export const buildMatchedIndicator = ({ throw new Error(`Expected indicator field to be an object, but found: ${indicator}`); } const atomic = get(matchedThreat?._source, query.value) as unknown; - const type = get(indicator, 'type') as unknown; const event = get(matchedThreat?._source, 'event') as unknown; return { ...indicator, event, - matched: { atomic, field: query.field, id: query.id, index: query.index, type }, + matched: { + atomic, + field: query.field, + id: query.id, + index: query.index, + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, }; }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts index e4ed05baeed778..f24bfc08b39e0f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts @@ -8,14 +8,16 @@ import { get, isEmpty } from 'lodash'; import { estypes } from '@elastic/elasticsearch'; -import { EVENT_ENRICHMENT_INDICATOR_FIELD_MAP } from '../../../../../../common/cti/constants'; -import { CtiEnrichment } from '../../../../../../common/search_strategy/security_solution/cti'; - -type EventField = keyof typeof EVENT_ENRICHMENT_INDICATOR_FIELD_MAP; -const validEventFields = Object.keys(EVENT_ENRICHMENT_INDICATOR_FIELD_MAP) as EventField[]; - -const isValidEventField = (field: string): field is EventField => - validEventFields.includes(field as EventField); +import { + ENRICHMENT_TYPES, + EVENT_ENRICHMENT_INDICATOR_FIELD_MAP, +} from '../../../../../../common/cti/constants'; +import { + CtiEnrichment, + EventField, + isValidEventField, + validEventFields, +} from '../../../../../../common/search_strategy/security_solution/cti'; export const buildIndicatorShouldClauses = ( eventFields: Record @@ -67,6 +69,7 @@ const buildIndicatorMatchedFields = ( 'matched.field': [eventField], 'matched.id': [hit._id], 'matched.index': [hit._index], + 'matched.type': [ENRICHMENT_TYPES.InvestigationTime], }; }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index e6a835462619c1..c64713575c1303 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -28,6 +28,7 @@ import { import { getCreateThreatMatchRulesSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock'; import { getThreatMatchingSchemaPartialMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks'; import { SIGNALS_TEMPLATE_VERSION } from '../../../../plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template'; +import { ENRICHMENT_TYPES } from '../../../../plugins/security_solution/common/cti/constants'; const format = (value: unknown): string => JSON.stringify(value, null, 2); @@ -425,7 +426,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978783', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'destination.ip', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, provider: 'geenensp', type: 'url', @@ -457,7 +458,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978783', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'destination.ip', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, provider: 'geenensp', type: 'url', @@ -519,7 +520,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978785', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'source.ip', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, port: 57324, provider: 'geenensp', @@ -544,7 +545,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978787', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'source.ip', - type: 'ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, provider: 'other_provider', type: 'ip', @@ -619,7 +620,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978785', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'source.ip', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, port: 57324, provider: 'geenensp', @@ -649,7 +650,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978785', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'source.port', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, port: 57324, provider: 'geenensp', @@ -674,7 +675,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978787', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'source.ip', - type: 'ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, provider: 'other_provider', type: 'ip', @@ -754,7 +755,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978783', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'destination.ip', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, provider: 'geenensp', type: 'url', @@ -785,7 +786,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978783', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'destination.ip', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, provider: 'geenensp', type: 'url', @@ -813,7 +814,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978785', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'source.ip', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, port: 57324, provider: 'geenensp', @@ -838,7 +839,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978785', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'source.port', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, port: 57324, provider: 'geenensp', diff --git a/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/data.json b/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/data.json index 11b5e9bd0828b5..543250ba174994 100644 --- a/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/data.json @@ -7,6 +7,9 @@ "@timestamp": "2021-02-22T21:00:49.337Z", "myhash": { "mysha256": "a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3" + }, + "source": { + "ip": "192.168.1.1" } } } diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json index 9573372d02e9c5..c5d382194027fa 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json @@ -47,7 +47,6 @@ "input": { "type": "httpjson" }, - "@timestamp": "2021-03-10T14:51:07.663Z", "ecs": { "version": "1.6.0" }, diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json new file mode 100644 index 00000000000000..0598fd7ba7c866 --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json @@ -0,0 +1,63 @@ +{ + "type": "doc", + "value": { + "id": "a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3", + "index": "filebeat-7.12.0-2021.03.11-000001", + "source": { + "@timestamp": "2021-06-27T14:51:05.766Z", + "agent": { + "ephemeral_id": "34c78500-8df5-4a07-ba87-1cc738b98431", + "hostname": "test", + "id": "08a3d064-8f23-41f3-84b2-f917f6ff9588", + "name": "test", + "type": "filebeat", + "version": "7.12.0" + }, + "fileset": { + "name": "abusemalware" + }, + "threatintel": { + "indicator": { + "first_seen": "2021-03-11T08:02:14.000Z", + "ip": "192.168.1.1", + "provider": "another_provider", + "type": "ip" + }, + "abusemalware": { + "virustotal": { + "result": "38 / 61", + "link": "https://www.virustotal.com/gui/file/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/detection/f-a04ac6d", + "percent": "62.30" + } + } + }, + "tags": ["threatintel-abusemalware", "forwarded"], + "input": { + "type": "httpjson" + }, + "ecs": { + "version": "1.6.0" + }, + "related": { + "hash": [ + "9b6c3518a91d23ed77504b5416bfb5b3", + "a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3", + "1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL" + ] + }, + "service": { + "type": "threatintel" + }, + "event": { + "reference": "https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/", + "ingested": "2021-03-11T14:51:09.809069Z", + "created": "2021-03-11T14:51:07.663Z", + "kind": "enrichment", + "module": "threatintel", + "category": "threat", + "type": "indicator", + "dataset": "threatintel.abusemalware" + } + } + } +} diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/mappings.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/mappings.json new file mode 100644 index 00000000000000..072318f7f4fc49 --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/mappings.json @@ -0,0 +1,822 @@ +{ + "type": "index", + "value": { + "aliases": { + "filebeat-7.12.0": { + "is_write_index": false + } + }, + "index": "filebeat-7.12.0-2021.03.11-000001", + "mappings": { + "_meta": { + "beat": "filebeat", + "version": "7.12.0" + }, + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "container.labels.*" + } + }, + { + "fields": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "fields.*" + } + }, + { + "docker.container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.container.labels.*" + } + }, + { + "kubernetes.labels.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.labels.*" + } + }, + { + "kubernetes.annotations.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.annotations.*" + } + }, + { + "kubernetes.service.selectors.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.service.selectors.*" + } + }, + { + "docker.attrs": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.attrs.*" + } + }, + { + "azure.activitylogs.identity.claims.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "azure.activitylogs.identity.claims.*" + } + }, + { + "azure.platformlogs.properties.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "azure.platformlogs.properties.*" + } + }, + { + "kibana.log.meta": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "kibana.log.meta.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "build": { + "properties": { + "original": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "apache": { + "properties": { + "access": { + "properties": { + "ssl": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "error": { + "properties": { + "module": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "fileset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "threatintel": { + "properties": { + "abusemalware": { + "properties": { + "file_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature": { + "ignore_above": 1024, + "type": "keyword" + }, + "urlhaus_download": { + "ignore_above": 1024, + "type": "keyword" + }, + "virustotal": { + "properties": { + "link": { + "ignore_above": 1024, + "type": "keyword" + }, + "percent": { + "type": "float" + }, + "result": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "abuseurl": { + "properties": { + "blacklists": { + "properties": { + "spamhaus_dbl": { + "ignore_above": 1024, + "type": "keyword" + }, + "surbl": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "larted": { + "type": "boolean" + }, + "reporter": { + "ignore_above": 1024, + "type": "keyword" + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "ignore_above": 1024, + "type": "keyword" + }, + "url_status": { + "ignore_above": 1024, + "type": "keyword" + }, + "urlhaus_reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "anomali": { + "properties": { + "content": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "indicator": { + "ignore_above": 1024, + "type": "keyword" + }, + "labels": { + "ignore_above": 1024, + "type": "keyword" + }, + "modified": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "object_marking_refs": { + "ignore_above": 1024, + "type": "keyword" + }, + "pattern": { + "ignore_above": 1024, + "type": "keyword" + }, + "title": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "valid_from": { + "type": "date" + } + } + }, + "indicator": { + "properties": { + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "confidence": { + "ignore_above": 1024, + "type": "keyword" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + }, + "ssdeep": { + "ignore_above": 1024, + "type": "keyword" + }, + "tlsh": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "imphash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "first_seen": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ip": { + "type": "ip" + }, + "last_seen": { + "type": "date" + }, + "marking": { + "properties": { + "tlp": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "matched": { + "properties": { + "atomic": { + "ignore_above": 1024, + "type": "keyword" + }, + "field": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "registry": { + "properties": { + "data": { + "properties": { + "strings": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "scanner_stats": { + "type": "long" + }, + "sightings": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "misp": { + "properties": { + "attribute": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "comment": { + "ignore_above": 1024, + "type": "keyword" + }, + "deleted": { + "type": "boolean" + }, + "disable_correlation": { + "type": "boolean" + }, + "distribution": { + "type": "long" + }, + "event_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "object_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "object_relation": { + "ignore_above": 1024, + "type": "keyword" + }, + "sharing_group_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "to_ids": { + "type": "boolean" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "attribute_count": { + "type": "long" + }, + "date": { + "type": "date" + }, + "disable_correlation": { + "type": "boolean" + }, + "distribution": { + "ignore_above": 1024, + "type": "keyword" + }, + "extends_uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "info": { + "ignore_above": 1024, + "type": "keyword" + }, + "locked": { + "type": "boolean" + }, + "org": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "local": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "org_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "orgc": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "local": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "orgc_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "proposal_email_lock": { + "type": "boolean" + }, + "publish_timestamp": { + "type": "date" + }, + "published": { + "type": "boolean" + }, + "sharing_group_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat_level_id": { + "type": "long" + }, + "timestamp": { + "type": "date" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "otx": { + "properties": { + "content": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "indicator": { + "ignore_above": 1024, + "type": "keyword" + }, + "title": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "settings": { + "index": { + "lifecycle": { + "name": "filebeat", + "rollover_alias": "filebeat-7.12.0" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "max_docvalue_fields_search": "200", + "number_of_replicas": "1", + "number_of_shards": "1", + "refresh_interval": "5s" + } + } + } +} From 345b9382b171f3a3de2af2f78148dd1837781d12 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Jun 2021 09:11:31 +0200 Subject: [PATCH 037/121] Update dependency @elastic/charts to v31 (#102078) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 1cdeed4189cd32..23a3e823b2e3c8 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "dependencies": { "@elastic/apm-rum": "^5.8.0", "@elastic/apm-rum-react": "^1.2.11", - "@elastic/charts": "30.1.0", + "@elastic/charts": "31.0.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13", "@elastic/ems-client": "7.14.0", diff --git a/yarn.lock b/yarn.lock index 10cbb9789ba984..a86eb52398d214 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1359,10 +1359,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@30.1.0": - version "30.1.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-30.1.0.tgz#eb9b3348c149ce13f74876738a9d2899b6b10067" - integrity sha512-aUfXRQYQopm+6O48tEO0v/w6fETYORGiSPBRtqlq5xPncZGhGnQbgXVNQsPngYqapnKpOupXAqzjopF+RJ4QWg== +"@elastic/charts@31.0.0": + version "31.0.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-31.0.0.tgz#c0b177313192bf5999835b8e01dc20471a8e7ef8" + integrity sha512-k8IJEwUBzzAjjkw1GDbt3laUfBoyc2t46+i1hs0Ni3ehb9jVntJSbYMBDReUDjWQSrUvrgHM6Xht1urBsdLS8Q== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -1381,7 +1381,7 @@ redux "^4.0.4" reselect "^4.0.0" resize-observer-polyfill "^1.5.1" - ts-debounce "^1.0.0" + ts-debounce "^3.0.0" utility-types "^3.10.0" uuid "^3.3.2" @@ -27119,10 +27119,10 @@ trough@^1.0.0: dependencies: glob "^6.0.4" -ts-debounce@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/ts-debounce/-/ts-debounce-1.0.0.tgz#e433301744ba75fe25466f7f23e1382c646aae6a" - integrity sha512-V+IzWj418IoqqxVJD6I0zjPtgIyvAJ8VyViqzcxZ0JRiJXsi5mCmy1yUKkWd2gUygT28a8JsVFCgqdrf2pLUHQ== +ts-debounce@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ts-debounce/-/ts-debounce-3.0.0.tgz#9beedf59c04de3b5bef8ff28bd6885624df357be" + integrity sha512-7jiRWgN4/8IdvCxbIwnwg2W0bbYFBH6BxFqBjMKk442t7+liF2Z1H6AUCcl8e/pD93GjPru+axeiJwFmRww1WQ== ts-dedent@^2.0.0: version "2.0.0" From b92d955b5648bc24916e2d9f7e5f65d568daa734 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Wed, 30 Jun 2021 00:27:58 -0700 Subject: [PATCH 038/121] [SECURITY] Adds security links to doc link service (#102676) --- ...-plugin-core-public.doclinksstart.links.md | 11 ++++++++++ ...kibana-plugin-core-public.doclinksstart.md | 2 +- .../public/doc_links/doc_links_service.ts | 22 +++++++++++++++++++ src/core/public/public.api.md | 11 ++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 8754d19e2fc138..3650fe970d8fcf 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -28,9 +28,13 @@ readonly links: { readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; + readonly suricataModule: string; + readonly zeekModule: string; }; readonly auditbeat: { readonly base: string; + readonly auditdModule: string; + readonly systemModule: string; }; readonly metricbeat: { readonly base: string; @@ -47,6 +51,9 @@ readonly links: { readonly heartbeat: { readonly base: string; }; + readonly libbeat: { + readonly getStarted: string; + }; readonly logstash: { readonly base: string; }; @@ -123,6 +130,10 @@ readonly links: { readonly siem: { readonly guide: string; readonly gettingStarted: string; + readonly ml: string; + readonly ruleChangeLog: string; + readonly detectionsReq: string; + readonly networkMap: string; }; readonly query: { readonly eql: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 7dbb8a58694853..4f66cc9a2c10f9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
readonly ecs: {
readonly guide: string;
};
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
readonly ecs: {
readonly guide: string;
};
} | | diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index fcc12f43ec5311..1efe1e560bce10 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -51,9 +51,13 @@ export class DocLinksService { elasticsearchOutput: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/elasticsearch-output.html`, startup: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-starting.html`, exportedFields: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/exported-fields.html`, + suricataModule: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-module-suricata.html`, + zeekModule: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-module-zeek.html`, }, auditbeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/auditbeat/${DOC_LINK_VERSION}`, + auditdModule: `${ELASTIC_WEBSITE_URL}guide/en/beats/auditbeat/${DOC_LINK_VERSION}/auditbeat-module-auditd.html`, + systemModule: `${ELASTIC_WEBSITE_URL}guide/en/beats/auditbeat/${DOC_LINK_VERSION}/auditbeat-module-system.html`, }, enterpriseSearch: { base: `${ELASTIC_WEBSITE_URL}guide/en/enterprise-search/${DOC_LINK_VERSION}`, @@ -70,6 +74,9 @@ export class DocLinksService { heartbeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/heartbeat/${DOC_LINK_VERSION}`, }, + libbeat: { + getStarted: `${ELASTIC_WEBSITE_URL}guide/en/beats/libbeat/${DOC_LINK_VERSION}/getting-started.html`, + }, logstash: { base: `${ELASTIC_WEBSITE_URL}guide/en/logstash/${DOC_LINK_VERSION}`, }, @@ -195,6 +202,10 @@ export class DocLinksService { siem: { guide: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`, gettingStarted: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`, + ml: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/machine-learning.html`, + ruleChangeLog: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/prebuilt-rules-changelog.html`, + detectionsReq: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/detections-permissions-section.html`, + networkMap: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/conf-map-ui.html`, }, query: { eql: `${ELASTICSEARCH_DOCS}eql.html`, @@ -451,9 +462,13 @@ export interface DocLinksStart { readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; + readonly suricataModule: string; + readonly zeekModule: string; }; readonly auditbeat: { readonly base: string; + readonly auditdModule: string; + readonly systemModule: string; }; readonly metricbeat: { readonly base: string; @@ -470,6 +485,9 @@ export interface DocLinksStart { readonly heartbeat: { readonly base: string; }; + readonly libbeat: { + readonly getStarted: string; + }; readonly logstash: { readonly base: string; }; @@ -546,6 +564,10 @@ export interface DocLinksStart { readonly siem: { readonly guide: string; readonly gettingStarted: string; + readonly ml: string; + readonly ruleChangeLog: string; + readonly detectionsReq: string; + readonly networkMap: string; }; readonly query: { readonly eql: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 3f8184bea97fee..f18dfb02fd41da 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -507,9 +507,13 @@ export interface DocLinksStart { readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; + readonly suricataModule: string; + readonly zeekModule: string; }; readonly auditbeat: { readonly base: string; + readonly auditdModule: string; + readonly systemModule: string; }; readonly metricbeat: { readonly base: string; @@ -526,6 +530,9 @@ export interface DocLinksStart { readonly heartbeat: { readonly base: string; }; + readonly libbeat: { + readonly getStarted: string; + }; readonly logstash: { readonly base: string; }; @@ -602,6 +609,10 @@ export interface DocLinksStart { readonly siem: { readonly guide: string; readonly gettingStarted: string; + readonly ml: string; + readonly ruleChangeLog: string; + readonly detectionsReq: string; + readonly networkMap: string; }; readonly query: { readonly eql: string; From 790bd35ea7716e6de03db35c382abfb83c39543a Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 30 Jun 2021 10:54:06 +0300 Subject: [PATCH 039/121] [TSVB] Fix TSVB is not reporting all categories of Elasticsearch error (#102926) * [TSVB] Fix TSVB is not reporting all categories of Elasticsearch error Closes: #94182 * move validation to server side Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/fields_utils.ts | 2 +- .../lib => common}/validate_interval.ts | 41 ++++---- .../components/timeseries_visualization.tsx | 12 --- .../application/components/vis_editor.tsx | 4 +- .../public/request_handler.ts | 13 +-- .../server/lib/get_vis_data.ts | 36 ++++++- .../get_interval_and_timefield.test.ts | 9 +- .../vis_data/get_interval_and_timefield.ts | 25 ++++- .../server/lib/vis_data/get_series_data.ts | 17 ++-- .../server/lib/vis_data/get_table_data.ts | 32 ++---- .../lib/vis_data/handle_error_response.js | 37 ------- .../vis_data/handle_error_response.test.ts | 99 +++++++++++++++++++ .../lib/vis_data/handle_error_response.ts | 57 +++++++++++ .../series/date_histogram.test.js | 11 ++- .../lib/vis_data/series/get_request_params.ts | 16 +-- .../vis_type_timeseries/server/types.ts | 16 ++- 16 files changed, 285 insertions(+), 142 deletions(-) rename src/plugins/vis_type_timeseries/{public/application/lib => common}/validate_interval.ts (52%) delete mode 100644 src/plugins/vis_type_timeseries/server/lib/vis_data/handle_error_response.js create mode 100644 src/plugins/vis_type_timeseries/server/lib/vis_data/handle_error_response.test.ts create mode 100644 src/plugins/vis_type_timeseries/server/lib/vis_data/handle_error_response.ts diff --git a/src/plugins/vis_type_timeseries/common/fields_utils.ts b/src/plugins/vis_type_timeseries/common/fields_utils.ts index 6a83dd323b3fdb..b64fcc383a1bb9 100644 --- a/src/plugins/vis_type_timeseries/common/fields_utils.ts +++ b/src/plugins/vis_type_timeseries/common/fields_utils.ts @@ -25,7 +25,7 @@ export class FieldNotFoundError extends Error { return this.constructor.name; } - public get body() { + public get errBody() { return this.message; } } diff --git a/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.ts b/src/plugins/vis_type_timeseries/common/validate_interval.ts similarity index 52% rename from src/plugins/vis_type_timeseries/public/application/lib/validate_interval.ts rename to src/plugins/vis_type_timeseries/common/validate_interval.ts index a602b34d999867..7f9ccf20c0eb1d 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.ts +++ b/src/plugins/vis_type_timeseries/common/validate_interval.ts @@ -7,20 +7,29 @@ */ import { i18n } from '@kbn/i18n'; -import { GTE_INTERVAL_RE } from '../../../common/interval_regexp'; -import { search } from '../../../../../plugins/data/public'; +import { GTE_INTERVAL_RE } from './interval_regexp'; +import { parseInterval, TimeRangeBounds } from '../../data/common'; -import type { TimeRangeBounds } from '../../../../data/common'; -import type { TimeseriesVisParams } from '../../types'; +export class ValidateIntervalError extends Error { + constructor() { + super( + i18n.translate('visTypeTimeseries.validateInterval.notifier.maxBucketsExceededErrorMessage', { + defaultMessage: + 'Your query attempted to fetch too much data. Reducing the time range or changing the interval used usually fixes the issue.', + }) + ); + } + + public get name() { + return this.constructor.name; + } -const { parseInterval } = search.aggs; + public get errBody() { + return this.message; + } +} -export function validateInterval( - bounds: TimeRangeBounds, - panel: TimeseriesVisParams, - maxBuckets: number -) { - const { interval } = panel; +export function validateInterval(bounds: TimeRangeBounds, interval: string, maxBuckets: number) { const { min, max } = bounds; // No need to check auto it will return around 100 if (!interval) return; @@ -33,15 +42,7 @@ export function validateInterval( const span = max!.valueOf() - min!.valueOf(); const buckets = Math.floor(span / duration.asMilliseconds()); if (buckets > maxBuckets) { - throw new Error( - i18n.translate( - 'visTypeTimeseries.validateInterval.notifier.maxBucketsExceededErrorMessage', - { - defaultMessage: - 'Your query attempted to fetch too much data. Reducing the time range or changing the interval used usually fixes the issue.', - } - ) - ); + throw new ValidateIntervalError(); } } } diff --git a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx index 5391bf319ee573..d97100a0cfaaf1 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx @@ -18,8 +18,6 @@ import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { PersistedState } from 'src/plugins/visualizations/public'; import { PaletteRegistry } from 'src/plugins/charts/public'; -// @ts-expect-error -import { ErrorComponent } from './error'; import { TimeseriesVisTypes } from './vis_types'; import type { TimeseriesVisData, PanelData } from '../../../common/types'; import { isVisSeriesData } from '../../../common/vis_data_utils'; @@ -147,16 +145,6 @@ function TimeseriesVisualization({ handlers.done(); }); - // Show the error panel - const error = isVisSeriesData(visData) && visData[model.id]?.error; - if (error) { - return ( -
- -
- ); - } - const VisComponent = TimeseriesVisTypes[model.type]; const isLastValueMode = diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx index 99f25643284025..d11b5a60b31b7d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx @@ -159,8 +159,8 @@ export class VisEditor extends Component { - this.visDataSubject.next(visData); + onDataChange = (data: { visData?: TimeseriesVisData }) => { + this.visDataSubject.next(data?.visData); }; render() { diff --git a/src/plugins/vis_type_timeseries/public/request_handler.ts b/src/plugins/vis_type_timeseries/public/request_handler.ts index 89b3cb3b6c5831..4cd297a597dfce 100644 --- a/src/plugins/vis_type_timeseries/public/request_handler.ts +++ b/src/plugins/vis_type_timeseries/public/request_handler.ts @@ -6,14 +6,13 @@ * Side Public License, v 1. */ -import { KibanaContext } from '../../data/public'; - import { getTimezone } from './application/lib/get_timezone'; -import { validateInterval } from './application/lib/validate_interval'; import { getUISettings, getDataStart, getCoreStart } from './services'; -import { MAX_BUCKETS_SETTING, ROUTES } from '../common/constants'; -import { TimeseriesVisParams } from './types'; +import { ROUTES } from '../common/constants'; + +import type { TimeseriesVisParams } from './types'; import type { TimeseriesVisData } from '../common/types'; +import type { KibanaContext } from '../../data/public'; interface MetricsRequestHandlerParams { input: KibanaContext | null; @@ -37,10 +36,6 @@ export const metricsRequestHandler = async ({ const parsedTimeRange = data.query.timefilter.timefilter.calculateBounds(input?.timeRange!); if (visParams && visParams.id && !visParams.isModelInvalid) { - const maxBuckets = config.get(MAX_BUCKETS_SETTING); - - validateInterval(parsedTimeRange, visParams, maxBuckets); - const untrackSearch = dataSearch.session.isCurrentSession(searchSessionId) && dataSearch.session.trackSearch({ diff --git a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts index dd45812f4ebfce..817812a88ca987 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts @@ -9,7 +9,7 @@ import _ from 'lodash'; import { Framework } from '../plugin'; -import type { TimeseriesVisData } from '../../common/types'; +import type { TimeseriesVisData, FetchedIndexPattern, Series } from '../../common/types'; import { PANEL_TYPES } from '../../common/enums'; import type { VisTypeTimeseriesVisDataRequest, @@ -20,6 +20,8 @@ import { getSeriesData } from './vis_data/get_series_data'; import { getTableData } from './vis_data/get_table_data'; import { getEsQueryConfig } from './vis_data/helpers/get_es_query_uisettings'; import { getCachedIndexPatternFetcher } from './search_strategies/lib/cached_index_pattern_fetcher'; +import { MAX_BUCKETS_SETTING } from '../../common/constants'; +import { getIntervalAndTimefield } from './vis_data/get_interval_and_timefield'; export async function getVisData( requestContext: VisTypeTimeseriesRequestHandlerContext, @@ -32,15 +34,41 @@ export async function getVisData( const esQueryConfig = await getEsQueryConfig(uiSettings); const promises = request.body.panels.map((panel) => { + const cachedIndexPatternFetcher = getCachedIndexPatternFetcher(indexPatternsService, { + fetchKibanaIndexForStringIndexes: Boolean(panel.use_kibana_indexes), + }); const services: VisTypeTimeseriesRequestServices = { esQueryConfig, esShardTimeout, indexPatternsService, uiSettings, + cachedIndexPatternFetcher, searchStrategyRegistry: framework.searchStrategyRegistry, - cachedIndexPatternFetcher: getCachedIndexPatternFetcher(indexPatternsService, { - fetchKibanaIndexForStringIndexes: Boolean(panel.use_kibana_indexes), - }), + buildSeriesMetaParams: async ( + index: FetchedIndexPattern, + useKibanaIndexes: boolean, + series?: Series + ) => { + /** This part of code is required to try to get the default timefield for string indices. + * The rest of the functionality available for Kibana indexes should not be active **/ + if (!useKibanaIndexes && index.indexPatternString) { + index = await cachedIndexPatternFetcher(index.indexPatternString, true); + } + + const maxBuckets = await uiSettings.get(MAX_BUCKETS_SETTING); + const { min, max } = request.body.timerange; + + return getIntervalAndTimefield( + panel, + index, + { + min, + max, + maxBuckets, + }, + series + ); + }, }; return panel.type === PANEL_TYPES.TABLE diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.ts index 0d1ca9cba022a7..62220e08b7e104 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.ts @@ -11,12 +11,17 @@ import { FetchedIndexPattern, Panel, Series } from '../../../common/types'; describe('getIntervalAndTimefield(panel, series)', () => { const index: FetchedIndexPattern = {} as FetchedIndexPattern; + const params = { + min: '2017-01-01T00:00:00Z', + max: '2017-01-01T01:00:00Z', + maxBuckets: 1000, + }; test('returns the panel interval and timefield', () => { const panel = { time_field: '@timestamp', interval: 'auto' } as Panel; const series = {} as Series; - expect(getIntervalAndTimefield(panel, index, series)).toEqual({ + expect(getIntervalAndTimefield(panel, index, params, series)).toEqual({ timeField: '@timestamp', interval: 'auto', }); @@ -30,7 +35,7 @@ describe('getIntervalAndTimefield(panel, series)', () => { series_time_field: 'time', } as unknown) as Series; - expect(getIntervalAndTimefield(panel, index, series)).toEqual({ + expect(getIntervalAndTimefield(panel, index, params, series)).toEqual({ timeField: 'time', interval: '1m', }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.ts index 0e90dfe77e8145..b7a22abd825e05 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.ts @@ -5,13 +5,25 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import moment from 'moment'; import { AUTO_INTERVAL } from '../../../common/constants'; import { validateField } from '../../../common/fields_utils'; +import { validateInterval } from '../../../common/validate_interval'; import type { FetchedIndexPattern, Panel, Series } from '../../../common/types'; -export function getIntervalAndTimefield(panel: Panel, index: FetchedIndexPattern, series?: Series) { +interface IntervalParams { + min: string; + max: string; + maxBuckets: number; +} + +export function getIntervalAndTimefield( + panel: Panel, + index: FetchedIndexPattern, + { min, max, maxBuckets }: IntervalParams, + series?: Series +) { const timeField = (series?.override_index_pattern ? series.series_time_field : panel.time_field) || index.indexPattern?.timeFieldName; @@ -28,6 +40,15 @@ export function getIntervalAndTimefield(panel: Panel, index: FetchedIndexPattern maxBars = series.series_max_bars; } + validateInterval( + { + min: moment.utc(min), + max: moment.utc(max), + }, + interval, + maxBuckets + ); + return { maxBars, timeField, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.ts index 822331e0ca0d07..8d495d68eb6257 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.ts @@ -8,8 +8,6 @@ import { i18n } from '@kbn/i18n'; -// not typed yet -// @ts-expect-error import { handleErrorResponse } from './handle_error_response'; import { getAnnotations } from './get_annotations'; import { handleResponseBody } from './series/handle_response_body'; @@ -51,6 +49,8 @@ export async function getSeriesData( uiRestrictions: capabilities.uiRestrictions, }; + const handleError = handleErrorResponse(panel); + try { const bodiesPromises = getActiveSeries(panel).map((series) => getSeriesRequestParams(req, panel, panelIndex, series, capabilities, services) @@ -97,14 +97,9 @@ export async function getSeriesData( }, }; } catch (err) { - if (err.body) { - err.response = err.body; - - return { - ...meta, - ...handleErrorResponse(panel)(err), - }; - } - return meta; + return { + ...meta, + ...handleError(err), + }; } } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts index db2e027f7815c3..3f8d30f0ed8339 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.ts @@ -12,20 +12,19 @@ import { get } from 'lodash'; // not typed yet // @ts-expect-error import { buildRequestBody } from './table/build_request_body'; -// @ts-expect-error import { handleErrorResponse } from './handle_error_response'; // @ts-expect-error import { processBucket } from './table/process_bucket'; import { createFieldsFetcher } from '../search_strategies/lib/fields_fetcher'; import { extractFieldLabel } from '../../../common/fields_utils'; + import type { VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesRequestServices, VisTypeTimeseriesVisDataRequest, } from '../../types'; import type { Panel } from '../../../common/types'; -import { getIntervalAndTimefield } from './get_interval_and_timefield'; export async function getTableData( requestContext: VisTypeTimeseriesRequestHandlerContext, @@ -67,23 +66,13 @@ export async function getTableData( return panel.pivot_id; }; - const buildSeriesMetaParams = async () => { - let index = panelIndex; - - /** This part of code is required to try to get the default timefield for string indices. - * The rest of the functionality available for Kibana indexes should not be active **/ - if (!panel.use_kibana_indexes && index.indexPatternString) { - index = await services.cachedIndexPatternFetcher(index.indexPatternString, true); - } - - return getIntervalAndTimefield(panel, index); - }; - const meta = { type: panel.type, uiRestrictions: capabilities.uiRestrictions, }; + const handleError = handleErrorResponse(panel); + try { const body = await buildRequestBody( req, @@ -92,7 +81,7 @@ export async function getTableData( panelIndex, capabilities, services.uiSettings, - buildSeriesMetaParams + () => services.buildSeriesMetaParams(panelIndex, Boolean(panel.use_kibana_indexes)) ); const [resp] = await searchStrategy.search(requestContext, req, [ @@ -121,14 +110,9 @@ export async function getTableData( series, }; } catch (err) { - if (err.body) { - err.response = err.body; - - return { - ...meta, - ...handleErrorResponse(panel)(err), - }; - } - return meta; + return { + ...meta, + ...handleError(err), + }; } } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/handle_error_response.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/handle_error_response.js deleted file mode 100644 index b5583ce20d68ae..00000000000000 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/handle_error_response.js +++ /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. - */ - -export const handleErrorResponse = (panel) => (error) => { - if (error.isBoom && error.status === 401) throw error; - const result = {}; - let errorResponse; - try { - errorResponse = JSON.parse(error.response); - } catch (e) { - errorResponse = error.response; - } - if (!errorResponse && !(error.name === 'KQLSyntaxError')) { - errorResponse = { - message: error.message, - stack: error.stack, - }; - } - if (error.name === 'KQLSyntaxError') { - errorResponse = { - message: error.shortMessage, - stack: error.stack, - }; - } - result[panel.id] = { - id: panel.id, - statusCode: error.statusCode, - error: errorResponse, - series: [], - }; - return result; -}; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/handle_error_response.test.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/handle_error_response.test.ts new file mode 100644 index 00000000000000..eeb22a3dc32cff --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/handle_error_response.test.ts @@ -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 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 { Panel } from '../../../common/types'; +import { handleErrorResponse, ErrorResponse } from './handle_error_response'; + +describe('handleErrorResponse', () => { + const handleError = handleErrorResponse(({ + id: 'test_panel', + } as unknown) as Panel); + + test('should only handle errors that contain errBody', () => { + expect(handleError(new Error('Test Error'))).toMatchInlineSnapshot(`Object {}`); + + expect(handleError({ errBody: 'test' } as ErrorResponse)).toMatchInlineSnapshot(` + Object { + "test_panel": Object { + "error": "test", + "id": "test_panel", + "series": Array [], + }, + } + `); + }); + + test('should set as error the last value of caused_by', () => { + expect( + handleError({ + errBody: { + error: { + reason: 'wrong 0', + caused_by: { + reason: 'wrong 1', + caused_by: { + caused_by: 'ok', + }, + }, + }, + }, + } as ErrorResponse) + ).toMatchInlineSnapshot(` + Object { + "test_panel": Object { + "error": "ok", + "id": "test_panel", + "series": Array [], + }, + } + `); + }); + + test('should use the previous error message if the actual value is empty', () => { + expect( + handleError({ + errBody: { + error: { + reason: 'ok', + caused_by: { + reason: '', + }, + }, + }, + } as ErrorResponse) + ).toMatchInlineSnapshot(` + Object { + "test_panel": Object { + "error": "ok", + "id": "test_panel", + "series": Array [], + }, + } + `); + }); + + test('shouldn not return empty error message', () => { + expect( + handleError({ + errBody: { + error: { + reason: '', + }, + }, + } as ErrorResponse) + ).toMatchInlineSnapshot(` + Object { + "test_panel": Object { + "error": "Unexpected error", + "id": "test_panel", + "series": Array [], + }, + } + `); + }); +}); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/handle_error_response.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/handle_error_response.ts new file mode 100644 index 00000000000000..d9327a0fbb786e --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/handle_error_response.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { i18n } from '@kbn/i18n'; +import type { Panel } from '../../../common/types'; + +type ErrorType = + | { + reason: string; + caused_by?: ErrorType; + } + | string; + +export type ErrorResponse = Error & + Partial<{ + errBody: + | { + error: ErrorType; + } + | string; + }>; + +const getErrorMessage = (errBody: ErrorType, defaultMessage?: string): string | undefined => { + if (typeof errBody === 'string') { + return errBody; + } else { + if (errBody.caused_by) { + return getErrorMessage(errBody.caused_by, errBody.reason); + } + return errBody.reason || defaultMessage; + } +}; + +export const handleErrorResponse = (panel: Panel) => (error: ErrorResponse) => { + const result: Record = {}; + + if (error.errBody) { + const errorResponse = + typeof error.errBody === 'string' ? error.errBody : getErrorMessage(error.errBody.error); + + result[panel.id] = { + id: panel.id, + error: + errorResponse ?? + i18n.translate('visTypeTimeseries.handleErrorResponse.unexpectedError', { + defaultMessage: 'Unexpected error', + }), + series: [], + }; + } + + return result; +}; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js index 08b9801254c2e2..022718ece435d9 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js @@ -47,7 +47,16 @@ describe('dateHistogram(req, panel, series)', () => { get: async (key) => (key === UI_SETTINGS.HISTOGRAM_MAX_BARS ? 100 : 50), }; buildSeriesMetaParams = jest.fn(async () => { - return getIntervalAndTimefield(panel, indexPattern, series); + return getIntervalAndTimefield( + panel, + indexPattern, + { + min: '2017-01-01T00:00:00Z', + max: '2017-01-01T01:00:00Z', + maxBuckets: 1000, + }, + series + ); }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts index a2248308dc5711..aedc4ee7c8a155 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/get_request_params.ts @@ -7,7 +7,6 @@ */ import { buildRequestBody } from './build_request_body'; -import { getIntervalAndTimefield } from '../get_interval_and_timefield'; import type { FetchedIndexPattern, Panel, Series } from '../../../../common/types'; import type { @@ -27,6 +26,7 @@ export async function getSeriesRequestParams( esShardTimeout, uiSettings, cachedIndexPatternFetcher, + buildSeriesMetaParams, }: VisTypeTimeseriesRequestServices ) { let seriesIndex = panelIndex; @@ -35,18 +35,6 @@ export async function getSeriesRequestParams( seriesIndex = await cachedIndexPatternFetcher(series.series_index_pattern ?? ''); } - const buildSeriesMetaParams = async () => { - let index = seriesIndex; - - /** This part of code is required to try to get the default timefield for string indices. - * The rest of the functionality available for Kibana indexes should not be active **/ - if (!panel.use_kibana_indexes && index.indexPatternString) { - index = await cachedIndexPatternFetcher(index.indexPatternString, true); - } - - return getIntervalAndTimefield(panel, index, series); - }; - const request = await buildRequestBody( req, panel, @@ -55,7 +43,7 @@ export async function getSeriesRequestParams( seriesIndex, capabilities, uiSettings, - buildSeriesMetaParams + () => buildSeriesMetaParams(seriesIndex, Boolean(panel.use_kibana_indexes), series) ); return { diff --git a/src/plugins/vis_type_timeseries/server/types.ts b/src/plugins/vis_type_timeseries/server/types.ts index 2fc46b7cd1f11c..a2657f99d222dc 100644 --- a/src/plugins/vis_type_timeseries/server/types.ts +++ b/src/plugins/vis_type_timeseries/server/types.ts @@ -6,17 +6,18 @@ * Side Public License, v 1. */ -import { Observable } from 'rxjs'; -import { SharedGlobalConfig } from 'kibana/server'; +import type { Observable } from 'rxjs'; +import type { SharedGlobalConfig } from 'kibana/server'; import type { IRouter, IUiSettingsClient, KibanaRequest } from 'src/core/server'; import type { DataRequestHandlerContext, EsQueryConfig, IndexPatternsService, } from '../../data/server'; -import type { VisPayload } from '../common/types'; +import type { Series, VisPayload } from '../common/types'; import type { SearchStrategyRegistry } from './lib/search_strategies'; import type { CachedIndexPatternFetcher } from './lib/search_strategies/lib/cached_index_pattern_fetcher'; +import type { FetchedIndexPattern } from '../common/types'; export type ConfigObservable = Observable; @@ -35,4 +36,13 @@ export interface VisTypeTimeseriesRequestServices { indexPatternsService: IndexPatternsService; searchStrategyRegistry: SearchStrategyRegistry; cachedIndexPatternFetcher: CachedIndexPatternFetcher; + buildSeriesMetaParams: ( + index: FetchedIndexPattern, + useKibanaIndexes: boolean, + series?: Series + ) => Promise<{ + maxBars: number; + timeField?: string; + interval: string; + }>; } From 21858a570da998ca10dec976788210a3c8d9680c Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Wed, 30 Jun 2021 10:06:38 +0200 Subject: [PATCH 040/121] [Lens] Fix value popover spacing (#103081) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../line_curve_option.tsx | 47 +++++++++---------- .../missing_values_option.tsx | 10 +--- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/line_curve_option.tsx b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/line_curve_option.tsx index ea0a1553ba5e51..1df7744524779b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/line_curve_option.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/line_curve_option.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiSpacer, EuiSwitch } from '@elastic/eui'; +import { EuiFormRow, EuiSwitch } from '@elastic/eui'; import { XYCurveType } from '../types'; export interface LineCurveOptionProps { @@ -28,29 +28,26 @@ export const LineCurveOption: React.FC = ({ isCurveTypeEnabled = true, }) => { return isCurveTypeEnabled ? ( - <> - - { - if (e.target.checked) { - onChange('CURVE_MONOTONE_X'); - } else { - onChange('LINEAR'); - } - }} - data-test-subj="lnsCurveStyleToggle" - /> - - - + + { + if (e.target.checked) { + onChange('CURVE_MONOTONE_X'); + } else { + onChange('LINEAR'); + } + }} + data-test-subj="lnsCurveStyleToggle" + /> + ) : null; }; diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_values_option.tsx b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_values_option.tsx index a683d4fbf514c3..fb6ecec4d28013 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_values_option.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_values_option.tsx @@ -7,14 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiButtonGroup, - EuiFormRow, - EuiIconTip, - EuiSuperSelect, - EuiText, - EuiSpacer, -} from '@elastic/eui'; +import { EuiButtonGroup, EuiFormRow, EuiIconTip, EuiSuperSelect, EuiText } from '@elastic/eui'; import { FittingFunction, fittingFunctionDefinitions } from '../fitting_functions'; import { ValueLabelConfig } from '../types'; @@ -140,7 +133,6 @@ export const MissingValuesOptions: React.FC = ({ /> )} - ); }; From 4aca0b7b61b8b5db51f1fc9d0e656f8f02d5ccfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 30 Jun 2021 10:20:31 +0200 Subject: [PATCH 041/121] =?UTF-8?q?[APM]=20Add=20=E2=80=9CAnalyze=20Data?= =?UTF-8?q?=E2=80=9D=20button=20(#103485)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Shahzad --- .../__snapshots__/apm_telemetry.test.ts.snap | 60 +++++++++++++ x-pack/plugins/apm/common/agent_name.test.ts | 34 ++++++- x-pack/plugins/apm/common/agent_name.ts | 15 +++- .../components/app/correlations/index.tsx | 1 + .../components/app/service_overview/index.tsx | 5 +- .../templates/apm_service_template.tsx | 78 +++++++++++++++- .../shared/agent_icon/get_agent_icon.ts | 5 ++ .../apm/server/lib/apm_telemetry/schema.ts | 3 + .../apm/typings/es_schemas/ui/fields/agent.ts | 2 + .../schema/xpack_plugins.json | 90 +++++++++++++++++++ 10 files changed, 282 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap index 71b0929164705b..a2baee60749897 100644 --- a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap @@ -16,6 +16,9 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the "dotnet": { "type": "long" }, + "iOS/swift": { + "type": "long" + }, "go": { "type": "long" }, @@ -70,6 +73,9 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the "opentelemetry/ruby": { "type": "long" }, + "opentelemetry/swift": { + "type": "long" + }, "opentelemetry/webjs": { "type": "long" } @@ -131,6 +137,60 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the } } }, + "iOS/swift": { + "properties": { + "agent": { + "properties": { + "version": { + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + }, + "composite": { + "type": "keyword" + } + } + }, + "language": { + "properties": { + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + }, + "composite": { + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + }, + "composite": { + "type": "keyword" + } + } + } + } + } + } + }, "go": { "properties": { "agent": { diff --git a/x-pack/plugins/apm/common/agent_name.test.ts b/x-pack/plugins/apm/common/agent_name.test.ts index 9f74136efe829c..162a5716d6c7b1 100644 --- a/x-pack/plugins/apm/common/agent_name.test.ts +++ b/x-pack/plugins/apm/common/agent_name.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isJavaAgentName, isRumAgentName } from './agent_name'; +import { isJavaAgentName, isRumAgentName, isIosAgentName } from './agent_name'; describe('agent name helpers', () => { describe('isJavaAgentName', () => { @@ -22,7 +22,7 @@ describe('agent name helpers', () => { }); describe('when the agent name is not java', () => { - it('returns true', () => { + it('returns false', () => { expect(isJavaAgentName('not java')).toEqual(false); }); }); @@ -47,9 +47,35 @@ describe('agent name helpers', () => { }); }); - describe('when the agent name something else', () => { + describe('when the agent name is something else', () => { + it('returns false', () => { + expect(isRumAgentName('not rum')).toEqual(false); + }); + }); + }); + + describe('isIosAgentName', () => { + describe('when the agent name is js-base', () => { + it('returns true', () => { + expect(isIosAgentName('iOS/swift')).toEqual(true); + }); + }); + + describe('when the agent name is rum-js', () => { it('returns true', () => { - expect(isRumAgentName('java')).toEqual(false); + expect(isIosAgentName('ios/swift')).toEqual(true); + }); + }); + + describe('when the agent name is opentelemetry/swift', () => { + it('returns true', () => { + expect(isIosAgentName('opentelemetry/swift')).toEqual(true); + }); + }); + + describe('when the agent name is something else', () => { + it('returns false', () => { + expect(isIosAgentName('not ios')).toEqual(false); }); }); }); diff --git a/x-pack/plugins/apm/common/agent_name.ts b/x-pack/plugins/apm/common/agent_name.ts index 36bfbabf7797d0..650e72751749e5 100644 --- a/x-pack/plugins/apm/common/agent_name.ts +++ b/x-pack/plugins/apm/common/agent_name.ts @@ -26,12 +26,14 @@ export const OPEN_TELEMETRY_AGENT_NAMES: AgentName[] = [ 'opentelemetry/php', 'opentelemetry/python', 'opentelemetry/ruby', + 'opentelemetry/swift', 'opentelemetry/webjs', ]; export const AGENT_NAMES: AgentName[] = [ 'dotnet', 'go', + 'iOS/swift', 'java', 'js-base', 'nodejs', @@ -62,7 +64,9 @@ export function isRumAgentName( return RUM_AGENT_NAMES.includes(agentName! as AgentName); } -export function normalizeAgentName(agentName: string | undefined) { +export function normalizeAgentName( + agentName: T +): T | string { if (isRumAgentName(agentName)) { return 'rum-js'; } @@ -71,5 +75,14 @@ export function normalizeAgentName(agentName: string | undefined) { return 'java'; } + if (isIosAgentName(agentName)) { + return 'ios'; + } + return agentName; } + +export function isIosAgentName(agentName?: string) { + const lowercased = agentName && agentName.toLowerCase(); + return lowercased === 'ios/swift' || lowercased === 'opentelemetry/swift'; +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/index.tsx b/x-pack/plugins/apm/public/components/app/correlations/index.tsx index 7b6328916d445e..36b298af834ac1 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/index.tsx @@ -131,6 +131,7 @@ export function Correlations() { return ( <> { setIsFlyoutVisible(true); }} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 5b202e208a52d9..fce543b05c6c3b 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import React from 'react'; import { useTrackPageview } from '../../../../../observability/public'; -import { isRumAgentName } from '../../../../common/agent_name'; +import { isRumAgentName, isIosAgentName } from '../../../../common/agent_name'; import { AnnotationsContextProvider } from '../../../context/annotations/annotations_context'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; @@ -43,6 +43,7 @@ export function ServiceOverview({ serviceName }: ServiceOverviewProps) { const { isMedium } = useBreakPoints(); const rowDirection = isMedium ? 'column' : 'row'; const isRumAgent = isRumAgentName(agentName); + const isIosAgent = isIosAgentName(agentName); return ( @@ -110,7 +111,7 @@ export function ServiceOverview({ serviceName }: ServiceOverviewProps) { )} - {!isRumAgent && ( + {!isRumAgent && !isIosAgent && ( [0] & { key: @@ -54,12 +70,12 @@ interface Props { export function ApmServiceTemplate(props: Props) { return ( -