From 868796a140dca01b3fa093d3b007bfde31b4a551 Mon Sep 17 00:00:00 2001 From: mitodrummer Date: Tue, 8 Mar 2022 17:40:12 -0800 Subject: [PATCH 01/13] alerts tab work. list view done --- .../plugins/session_view/common/constants.ts | 13 ++ .../index.test.tsx | 61 ++++++++ .../detail_panel_alert_list_item/index.tsx | 148 ++++++++++++++++++ .../detail_panel_alert_list_item/styles.ts | 104 ++++++++++++ .../detail_panel_alert_tab/index.test.tsx | 88 +++++++++++ .../detail_panel_alert_tab/index.tsx | 55 +++++++ .../detail_panel_alert_tab/styles.ts | 37 +++++ .../public/components/process_tree/hooks.ts | 23 ++- .../public/components/process_tree/index.tsx | 3 + .../public/components/session_view/hooks.ts | 37 ++++- .../public/components/session_view/index.tsx | 23 ++- .../public/components/session_view/styles.ts | 11 ++ .../session_view_detail_panel/index.tsx | 30 +++- .../server/routes/alerts_route.test.ts | 49 ++++++ .../server/routes/alerts_route.ts | 69 ++++++++ .../session_view/server/routes/index.ts | 2 + .../server/routes/process_events_route.ts | 14 +- 17 files changed, 734 insertions(+), 33 deletions(-) create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.test.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts create mode 100644 x-pack/plugins/session_view/server/routes/alerts_route.test.ts create mode 100644 x-pack/plugins/session_view/server/routes/alerts_route.ts diff --git a/x-pack/plugins/session_view/common/constants.ts b/x-pack/plugins/session_view/common/constants.ts index 5baf690dc44a5..c0250cb48e70c 100644 --- a/x-pack/plugins/session_view/common/constants.ts +++ b/x-pack/plugins/session_view/common/constants.ts @@ -6,6 +6,7 @@ */ export const PROCESS_EVENTS_ROUTE = '/internal/session_view/process_events_route'; +export const ALERTS_ROUTE = '/internal/session_view/alerts_route'; export const SESSION_ENTRY_LEADERS_ROUTE = '/internal/session_view/session_entry_leaders_route'; export const PROCESS_EVENTS_INDEX = 'logs-endpoint.events.process-default'; export const ALERTS_INDEX = '.siem-signals-default'; @@ -25,3 +26,15 @@ export const ENTRY_SESSION_ENTITY_ID_PROPERTY = 'process.entry_leader.entity_id' // search functionality will instead use a separate ES backend search to avoid this. // 3. Fewer round trips to the backend! export const PROCESS_EVENTS_PER_PAGE = 1000; + +// As an initial approach, we won't be implementing pagination for alerts. +// Instead we will load this fixed amount of alerts as a maximum for a session. +// This could cause an edge case, where a noisy rule that alerts on every process event +// causes a session to only list and highlight up to 1000 alerts, even though there could +// be far greater than this amount. UX should be added to let the end user know this is +// happening and to revise their rule to be more specific. +export const ALERTS_PER_PAGE = 1000; + +// when showing the count of alerts in details panel tab, if the number +// exceeds ALERT_COUNT_THRESHOLD we put a + next to it, e.g 999+ +export const ALERT_COUNT_THRESHOLD = 999; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.test.tsx new file mode 100644 index 0000000000000..e6572a097d85a --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.test.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { DetailPanelListItem } from './index'; + +const TEST_STRING = 'item title'; +const TEST_CHILD = {TEST_STRING}; +const TEST_COPY_STRING = 'test copy button'; +const BUTTON_TEST_ID = 'sessionView:test-copy-button'; +const TEST_COPY = ; +const LIST_ITEM_TEST_ID = 'sessionView:detail-panel-list-item'; +const WAIT_TIMEOUT = 500; + +describe('DetailPanelListItem component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When DetailPanelListItem is mounted', () => { + it('renders DetailPanelListItem correctly', async () => { + renderResult = mockedContext.render({TEST_CHILD}); + + expect(renderResult.queryByTestId(LIST_ITEM_TEST_ID)).toBeVisible(); + expect(renderResult.queryByText(TEST_STRING)).toBeVisible(); + }); + + it('renders copy element correctly', async () => { + renderResult = mockedContext.render( + {TEST_CHILD} + ); + + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + fireEvent.mouseEnter(renderResult.getByTestId(LIST_ITEM_TEST_ID)); + await waitFor(() => screen.queryByTestId(BUTTON_TEST_ID)); + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeVisible(); + + fireEvent.mouseLeave(renderResult.getByTestId(LIST_ITEM_TEST_ID)); + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + }); + + it('does not have mouse events when copy prop is not present', async () => { + renderResult = mockedContext.render({TEST_CHILD}); + + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + fireEvent.mouseEnter(renderResult.getByTestId(LIST_ITEM_TEST_ID)); + await waitFor(() => screen.queryByTestId(BUTTON_TEST_ID), { timeout: WAIT_TIMEOUT }); + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx new file mode 100644 index 0000000000000..1e8aaa44fa04b --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx @@ -0,0 +1,148 @@ +/* + * 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, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiSpacer, + EuiIcon, + EuiText, + EuiAccordion, + EuiPanel, + EuiPopover, + EuiContextMenuPanel, + EuiButtonIcon, + EuiContextMenuItem, +} from '@elastic/eui'; +import { Process, ProcessEvent } from '../../../common/types/process_tree'; +import { ProcessImpl } from '../process_tree/hooks'; +import { useStyles } from './styles'; + +interface DetailPanelAlertsListItemDeps { + event: ProcessEvent; + onProcessSelected: (process: Process) => void; + isInvestigated?: boolean; +} + +/** + * Detail panel description list item. + */ +export const DetailPanelAlertListItem = ({ + event, + onProcessSelected, + isInvestigated = false, +}: DetailPanelAlertsListItemDeps) => { + const styles = useStyles({ isInvestigated }); + const [isPopoverOpen, setPopover] = useState(false); + + const onClosePopover = useCallback(() => { + setPopover(false); + }, []); + + const onToggleMenu = useCallback(() => { + setPopover(!isPopoverOpen); + }, [isPopoverOpen]); + + const onJumpToAlert = () => { + const process = new ProcessImpl(event.process.entity_id); + process.addEvent(event); + + onProcessSelected(process); + setPopover(false); + }; + + const onShowDetails = useCallback(() => { + // TODO: call into alert flyout + setPopover(false); + }, []); + + if (!event.kibana) { + return null; + } + + const timestamp = event['@timestamp']; + const { uuid, name } = event.kibana.alert.rule; + const { args } = event.process; + + const menuItems = [ + + + , + + + , + ]; + + const forceState = !isInvestigated ? 'open' : undefined; + + return ( + +

+ + {name} +

+ + } + initialIsOpen={true} + forceState={forceState} + css={styles.alertItem} + extraAction={ + + } + isOpen={isPopoverOpen} + closePopover={onClosePopover} + panelPaddingSize="none" + anchorPosition="leftCenter" + > + + + } + > + + + {timestamp} + + + {args.join(' ')} + + {isInvestigated && ( +
+ + + +
+ )} +
+ ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts new file mode 100644 index 0000000000000..fa8267e81ebc4 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useEuiTheme, transparentize } from '@elastic/eui'; +import { CSSObject, css } from '@emotion/react'; + +interface StylesDeps { + isInvestigated: boolean; +} + +export const useStyles = ({ isInvestigated }: StylesDeps) => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const { colors, font, size, border } = euiTheme; + + const dangerBorder = transparentize(colors.danger, 0.2); + const dangerBackground = transparentize(colors.danger, 0.08); + const borderThickness = border.width.thin; + const mediumPadding = size.m; + + let alertTitleColor = colors.text; + let borderColor = colors.lightShade; + + if (isInvestigated) { + alertTitleColor = colors.primaryText; + borderColor = dangerBorder; + } + + const alertItem = css` + border: ${borderThickness} solid ${borderColor}; + padding: ${mediumPadding}; + border-radius: ${border.radius.medium}; + margin: 0 ${mediumPadding} ${mediumPadding} ${mediumPadding}; + background-color: ${colors.emptyShade}; + + & .euiAccordion__buttonContent { + width: 100%; + } + + & .euiAccordion__button { + min-width: 0; + width: calc(100% - ${size.l}); + } + + & .euiAccordion__childWrapper { + overflow: visible; + } + `; + + const alertTitle: CSSObject = { + color: alertTitleColor, + fontWeight: font.weight.semiBold, + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + }; + + const alertIcon: CSSObject = { + marginRight: size.s, + }; + + const alertAccordionButton: CSSObject = { + width: `calc(100% - ${size.l})`, + minWidth: 0, + }; + + const processPanel: CSSObject = { + border: `${borderThickness} solid ${colors.lightShade}`, + fontFamily: font.familyCode, + marginTop: mediumPadding, + padding: `${size.xs} ${size.s}`, + }; + + const investigatedLabel: CSSObject = { + position: 'relative', + zIndex: 1, + bottom: `-${mediumPadding}`, + left: `-${mediumPadding}`, + width: `calc(100% + ${mediumPadding} * 2)`, + borderTop: `${borderThickness} solid ${dangerBorder}`, + borderBottomLeftRadius: border.radius.medium, + borderBottomRightRadius: border.radius.medium, + backgroundColor: dangerBackground, + textAlign: 'center', + }; + + return { + alertItem, + alertTitle, + alertIcon, + alertAccordionButton, + processPanel, + investigatedLabel, + }; + }, [euiTheme, isInvestigated]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx new file mode 100644 index 0000000000000..2df9f47e5a416 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.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 { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { ProcessEventHost } from '../../../common/types/process_tree'; +import { DetailPanelHostTab } from './index'; + +const TEST_ARCHITECTURE = 'x86_64'; +const TEST_HOSTNAME = 'host-james-fleet-714-2'; +const TEST_ID = '48c1b3f1ac5da4e0057fc9f60f4d1d5d'; +const TEST_IP = '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809'; +const TEST_MAC = '42:01:0a:84:00:32'; +const TEST_NAME = 'name-james-fleet-714-2'; +const TEST_OS_FAMILY = 'family-centos'; +const TEST_OS_FULL = 'full-CentOS 7.9.2009'; +const TEST_OS_KERNEL = '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021'; +const TEST_OS_NAME = 'os-Linux'; +const TEST_OS_PLATFORM = 'platform-centos'; +const TEST_OS_VERSION = 'version-7.9.2009'; + +const TEST_HOST: ProcessEventHost = { + architecture: TEST_ARCHITECTURE, + hostname: TEST_HOSTNAME, + id: TEST_ID, + ip: TEST_IP, + mac: TEST_MAC, + name: TEST_NAME, + os: { + family: TEST_OS_FAMILY, + full: TEST_OS_FULL, + kernel: TEST_OS_KERNEL, + name: TEST_OS_NAME, + platform: TEST_OS_PLATFORM, + version: TEST_OS_VERSION, + }, +}; + +describe('DetailPanelHostTab component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When DetailPanelHostTab is mounted', () => { + it('renders DetailPanelHostTab correctly', async () => { + renderResult = mockedContext.render(); + + expect(renderResult.queryByText('architecture')).toBeVisible(); + expect(renderResult.queryByText('hostname')).toBeVisible(); + expect(renderResult.queryByText('id')).toBeVisible(); + expect(renderResult.queryByText('ip')).toBeVisible(); + expect(renderResult.queryByText('mac')).toBeVisible(); + expect(renderResult.queryByText('name')).toBeVisible(); + expect(renderResult.queryByText(TEST_ARCHITECTURE)).toBeVisible(); + expect(renderResult.queryByText(TEST_HOSTNAME)).toBeVisible(); + expect(renderResult.queryByText(TEST_ID)).toBeVisible(); + expect(renderResult.queryByText(TEST_IP)).toBeVisible(); + expect(renderResult.queryByText(TEST_MAC)).toBeVisible(); + expect(renderResult.queryByText(TEST_NAME)).toBeVisible(); + + // expand host os accordion + renderResult + .queryByTestId('sessionView:detail-panel-accordion') + ?.querySelector('button') + ?.click(); + expect(renderResult.queryByText('os.family')).toBeVisible(); + expect(renderResult.queryByText('os.full')).toBeVisible(); + expect(renderResult.queryByText('os.kernel')).toBeVisible(); + expect(renderResult.queryByText('os.name')).toBeVisible(); + expect(renderResult.queryByText('os.platform')).toBeVisible(); + expect(renderResult.queryByText('os.version')).toBeVisible(); + expect(renderResult.queryByText(TEST_OS_FAMILY)).toBeVisible(); + expect(renderResult.queryByText(TEST_OS_FULL)).toBeVisible(); + expect(renderResult.queryByText(TEST_OS_KERNEL)).toBeVisible(); + expect(renderResult.queryByText(TEST_OS_NAME)).toBeVisible(); + expect(renderResult.queryByText(TEST_OS_PLATFORM)).toBeVisible(); + expect(renderResult.queryByText(TEST_OS_VERSION)).toBeVisible(); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx new file mode 100644 index 0000000000000..797f8bf904638 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiHorizontalRule } from '@elastic/eui'; +import { ProcessEvent, Process } from '../../../common/types/process_tree'; +import { useStyles } from './styles'; +import { DetailPanelAlertListItem } from '../detail_panel_alert_list_item'; + +interface DetailPanelAlertTabDeps { + alerts: ProcessEvent[]; + onProcessSelected: (process: Process) => void; + investigatedAlert?: ProcessEvent; +} + +/** + * Host Panel of session view detail panel. + */ +export const DetailPanelAlertTab = ({ + alerts, + onProcessSelected, + investigatedAlert, +}: DetailPanelAlertTabDeps) => { + const styles = useStyles(); + + investigatedAlert = alerts[0]; + + return ( +
+ {investigatedAlert && ( +
+ + +
+ )} + {alerts.map((event) => { + const isInvestigatedAlert = + event.kibana?.alert.uuid === investigatedAlert?.kibana?.alert.uuid; + + if (isInvestigatedAlert) { + return null; + } + + return ; + })} +
+ ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts new file mode 100644 index 0000000000000..a4a85c33b258c --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useEuiTheme } from '@elastic/eui'; +import { CSSObject } from '@emotion/react'; + +export const useStyles = () => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const { colors, size } = euiTheme; + + const container: CSSObject = { + position: 'relative', + }; + + const stickyItem: CSSObject = { + position: 'sticky', + top: 0, + zIndex: 1, + backgroundColor: colors.emptyShade, + paddingTop: size.base, + }; + + return { + container, + stickyItem, + }; + }, [euiTheme]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts index a8c6ffe8e75d3..5ca403f7f5e8a 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts @@ -15,12 +15,18 @@ import { ProcessMap, ProcessEventsPage, } from '../../../common/types/process_tree'; -import { processNewEvents, searchProcessTree, autoExpandProcessTree } from './helpers'; +import { + processNewEvents, + searchProcessTree, + autoExpandProcessTree, + updateProcessMap, +} from './helpers'; import { sortProcesses } from '../../../common/utils/sort_processes'; interface UseProcessTreeDeps { sessionEntityId: string; data: ProcessEventsPage[]; + alerts: ProcessEvent[]; searchQuery?: string; } @@ -182,7 +188,12 @@ export class ProcessImpl implements Process { }); } -export const useProcessTree = ({ sessionEntityId, data, searchQuery }: UseProcessTreeDeps) => { +export const useProcessTree = ({ + sessionEntityId, + data, + alerts, + searchQuery, +}: UseProcessTreeDeps) => { // initialize map, as well as a placeholder for session leader process // we add a fake session leader event, sourced from wide event data. // this is because we might not always have a session leader event @@ -241,6 +252,14 @@ export const useProcessTree = ({ sessionEntityId, data, searchQuery }: UseProces } }, [data, processMap, orphans, processedPages, sessionEntityId]); + useEffect(() => { + // currently we are loading a single page of alerts, with no pagination + // so we only need to add these alert events to processMap once. + updateProcessMap(processMap, alerts); + // omitting processMap to avoid a infinite loop + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [alerts]); + useEffect(() => { setSearchResults(searchProcessTree(processMap, searchQuery)); autoExpandProcessTree(processMap); diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.tsx index 6b3061a0d77bb..68d33c16d9995 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree/index.tsx @@ -20,6 +20,7 @@ interface ProcessTreeDeps { sessionEntityId: string; data: ProcessEventsPage[]; + alerts: ProcessEvent[]; jumpToEvent?: ProcessEvent; isFetching: boolean; @@ -40,6 +41,7 @@ interface ProcessTreeDeps { export const ProcessTree = ({ sessionEntityId, data, + alerts, jumpToEvent, isFetching, hasNextPage, @@ -56,6 +58,7 @@ export const ProcessTree = ({ const { sessionLeader, processMap, searchResults } = useProcessTree({ sessionEntityId, data, + alerts, searchQuery, }); diff --git a/x-pack/plugins/session_view/public/components/session_view/hooks.ts b/x-pack/plugins/session_view/public/components/session_view/hooks.ts index b93e5b43ddf88..e4fbba08933fc 100644 --- a/x-pack/plugins/session_view/public/components/session_view/hooks.ts +++ b/x-pack/plugins/session_view/public/components/session_view/hooks.ts @@ -5,12 +5,16 @@ * 2.0. */ import { useEffect, useState } from 'react'; -import { useInfiniteQuery } from 'react-query'; +import { useInfiniteQuery, useQuery } from 'react-query'; import { EuiSearchBarOnChangeArgs } from '@elastic/eui'; import { CoreStart } from 'kibana/public'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { ProcessEvent, ProcessEventResults } from '../../../common/types/process_tree'; -import { PROCESS_EVENTS_ROUTE, PROCESS_EVENTS_PER_PAGE } from '../../../common/constants'; +import { + PROCESS_EVENTS_ROUTE, + PROCESS_EVENTS_PER_PAGE, + ALERTS_ROUTE, +} from '../../../common/constants'; export const useFetchSessionViewProcessEvents = ( sessionEntityId: string, @@ -43,7 +47,7 @@ export const useFetchSessionViewProcessEvents = ( return { events, cursor }; }, { - getNextPageParam: (lastPage, pages) => { + getNextPageParam: (lastPage) => { if (lastPage.events.length === PROCESS_EVENTS_PER_PAGE) { return { cursor: lastPage.events[lastPage.events.length - 1]['@timestamp'], @@ -51,7 +55,7 @@ export const useFetchSessionViewProcessEvents = ( }; } }, - getPreviousPageParam: (firstPage, pages) => { + getPreviousPageParam: (firstPage) => { if (jumpToEvent && firstPage.events.length === PROCESS_EVENTS_PER_PAGE) { return { cursor: firstPage.events[0]['@timestamp'], @@ -74,6 +78,31 @@ export const useFetchSessionViewProcessEvents = ( return query; }; +export const useFetchSessionViewAlerts = (sessionEntityId: string) => { + const { http } = useKibana().services; + const query = useQuery( + 'sessionViewAlerts', + async () => { + const res = await http.get(ALERTS_ROUTE, { + query: { + sessionEntityId, + }, + }); + + const events = res.events.map((event: any) => event._source as ProcessEvent); + + return events; + }, + { + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + } + ); + + return query; +}; + export const useSearchQuery = () => { const [searchQuery, setSearchQuery] = useState(''); const onSearch = ({ query }: EuiSearchBarOnChangeArgs) => { diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx index 7a82edc94ff1b..be89398f7f885 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -19,7 +19,7 @@ import { Process, ProcessEvent } from '../../../common/types/process_tree'; import { SessionViewDetailPanel } from '../session_view_detail_panel'; import { SessionViewSearchBar } from '../session_view_search_bar'; import { useStyles } from './styles'; -import { useFetchSessionViewProcessEvents } from './hooks'; +import { useFetchSessionViewProcessEvents, useFetchSessionViewAlerts } from './hooks'; interface SessionViewDeps { // the root node of the process tree to render. e.g process.entry.entity_id or process.session_leader.entity_id @@ -41,6 +41,10 @@ export const SessionView = ({ sessionEntityId, height, jumpToEvent }: SessionVie setSelectedProcess(process); }, []); + const toggleDetailPanel = () => { + setIsDetailOpen(!isDetailOpen); + }; + const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState(null); @@ -54,12 +58,13 @@ export const SessionView = ({ sessionEntityId, height, jumpToEvent }: SessionVie hasPreviousPage, } = useFetchSessionViewProcessEvents(sessionEntityId, jumpToEvent); - const hasData = data && data.pages.length > 0 && data.pages[0].events.length > 0; - const renderIsLoading = isFetching && !data; + const alertsQuery = useFetchSessionViewAlerts(sessionEntityId); + const { data: alerts, error: alertsError, isFetching: alertsFetching } = alertsQuery; + + const hasData = data && alerts && data.pages.length > 0 && data.pages[0].events.length > 0; + const hasError = error || alertsError; + const renderIsLoading = (isFetching || alertsFetching) && !data; const renderDetails = isDetailOpen && selectedProcess; - const toggleDetailPanel = () => { - setIsDetailOpen(!isDetailOpen); - }; if (!isFetching && !hasData) { return ( @@ -130,7 +135,7 @@ export const SessionView = ({ sessionEntityId, height, jumpToEvent }: SessionVie )} - {error && ( + {hasError && ( - + diff --git a/x-pack/plugins/session_view/public/components/session_view/styles.ts b/x-pack/plugins/session_view/public/components/session_view/styles.ts index d7159ec5b1b39..0109ba8191d10 100644 --- a/x-pack/plugins/session_view/public/components/session_view/styles.ts +++ b/x-pack/plugins/session_view/public/components/session_view/styles.ts @@ -17,6 +17,10 @@ export const useStyles = ({ height = 500 }: StylesDeps) => { const { euiTheme } = useEuiTheme(); const cached = useMemo(() => { + const { border, colors } = euiTheme; + + const thinBorder = `${border.width.thin} solid ${colors.lightShade}!important`; + const processTree: CSSObject = { height: `${height}px`, paddingTop: euiTheme.size.s, @@ -24,11 +28,18 @@ export const useStyles = ({ height = 500 }: StylesDeps) => { const detailPanel: CSSObject = { height: `${height}px`, + borderLeft: thinBorder, + borderRight: thinBorder, + }; + + const resizeHandle: CSSObject = { + zIndex: 2, }; return { processTree, detailPanel, + resizeHandle, }; }, [height, euiTheme]); diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx index a47ce1d91ac97..2ac1c37cbac44 100644 --- a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx @@ -7,23 +7,38 @@ import React, { useState, useMemo, useCallback } from 'react'; import { EuiTabs, EuiTab, EuiNotificationBadge } from '@elastic/eui'; import { EuiTabProps } from '../../types'; -import { Process } from '../../../common/types/process_tree'; +import { Process, ProcessEvent } from '../../../common/types/process_tree'; import { getDetailPanelProcess, getSelectedTabContent } from './helpers'; import { DetailPanelProcessTab } from '../detail_panel_process_tab'; import { DetailPanelHostTab } from '../detail_panel_host_tab'; +import { DetailPanelAlertTab } from '../detail_panel_alert_tab'; +import { ALERT_COUNT_THRESHOLD } from '../../../common/constants'; interface SessionViewDetailPanelDeps { + alerts: ProcessEvent[] | undefined; selectedProcess: Process; - onProcessSelected?: (process: Process) => void; + onProcessSelected: (process: Process) => void; } /** * Detail panel in the session view. */ -export const SessionViewDetailPanel = ({ selectedProcess }: SessionViewDetailPanelDeps) => { +export const SessionViewDetailPanel = ({ + alerts, + selectedProcess, + onProcessSelected, +}: SessionViewDetailPanelDeps) => { const [selectedTabId, setSelectedTabId] = useState('process'); const processDetail = useMemo(() => getDetailPanelProcess(selectedProcess), [selectedProcess]); + const getAlertCount = useCallback(() => { + if (!alerts) { + return; + } + + return alerts.length >= ALERT_COUNT_THRESHOLD ? ALERT_COUNT_THRESHOLD + '+' : alerts.length; + }, [alerts]); + const tabs: EuiTabProps[] = useMemo( () => [ { @@ -38,17 +53,18 @@ export const SessionViewDetailPanel = ({ selectedProcess }: SessionViewDetailPan }, { id: 'alerts', - disabled: true, name: 'Alerts', append: ( - 10 + {getAlertCount()} ), - content: null, + content: alerts && ( + + ), }, ], - [processDetail, selectedProcess.events] + [alerts, getAlertCount, processDetail, selectedProcess.events, onProcessSelected] ); const onSelectedTabChanged = useCallback((id: string) => { diff --git a/x-pack/plugins/session_view/server/routes/alerts_route.test.ts b/x-pack/plugins/session_view/server/routes/alerts_route.test.ts new file mode 100644 index 0000000000000..444f7a8f94675 --- /dev/null +++ b/x-pack/plugins/session_view/server/routes/alerts_route.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { doSearch } from './alerts_route'; +import { mockEvents } from '../../common/mocks/constants/session_view_process.mock'; + +const getEmptyResponse = async () => { + return { + hits: { + total: { value: 0, relation: 'eq' }, + hits: [], + }, + }; +}; + +const getResponse = async () => { + return { + hits: { + total: { value: mockEvents.length, relation: 'eq' }, + hits: mockEvents.map((event) => { + return { _source: event }; + }), + }, + }; +}; + +describe('alerts_route.ts', () => { + describe('doSearch(client, sessionEntityId)', () => { + it('should return an empty events array for a non existant entity_id', async () => { + const client = elasticsearchServiceMock.createElasticsearchClient(getEmptyResponse()); + + const body = await doSearch(client, 'asdf'); + + expect(body.alerts.length).toBe(0); + }); + + it('returns results for a particular session entity_id', async () => { + const client = elasticsearchServiceMock.createElasticsearchClient(getResponse()); + + const body = await doSearch(client, 'mockId'); + + expect(body.alerts.length).toBe(mockEvents.length); + }); + }); +}); diff --git a/x-pack/plugins/session_view/server/routes/alerts_route.ts b/x-pack/plugins/session_view/server/routes/alerts_route.ts new file mode 100644 index 0000000000000..7fed65b49e1bf --- /dev/null +++ b/x-pack/plugins/session_view/server/routes/alerts_route.ts @@ -0,0 +1,69 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import type { ElasticsearchClient } from 'kibana/server'; +import { IRouter } from '../../../../../src/core/server'; +import { + ALERTS_ROUTE, + ALERTS_PER_PAGE, + ALERTS_INDEX, + ENTRY_SESSION_ENTITY_ID_PROPERTY, +} from '../../common/constants'; +import { expandDottedObject } from '../../common/utils/expand_dotted_object'; + +export const registerAlertsRoute = (router: IRouter) => { + router.get( + { + path: ALERTS_ROUTE, + validate: { + query: schema.object({ + sessionEntityId: schema.string(), + }), + }, + }, + async (context, request, response) => { + const client = context.core.elasticsearch.client.asCurrentUser; + const { sessionEntityId } = request.query; + const body = await doSearch(client, sessionEntityId); + + return response.ok({ body }); + } + ); +}; + +export const doSearch = async (client: ElasticsearchClient, sessionEntityId: string) => { + const search = await client.search({ + index: [ALERTS_INDEX], + ignore_unavailable: true, // on a new installation the .siem-signals-default index might not be created yet. + body: { + query: { + match: { + [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId, + }, + }, + // This runtime_mappings is a temporary fix, so we are able to Query these ECS fields while they are not available + // TODO: Remove the runtime_mappings once process.entry_leader.entity_id is implemented to ECS + runtime_mappings: { + [ENTRY_SESSION_ENTITY_ID_PROPERTY]: { + type: 'keyword', + }, + }, + size: ALERTS_PER_PAGE, + sort: [{ '@timestamp': 'asc' }], + }, + }); + + const events = search.hits.hits.map((hit: any) => { + // TODO: re-eval if this is needed after updated ECS mappings are applied. + // the .siem-signals-default index flattens many properties. this util unflattens them. + hit._source = expandDottedObject(hit._source); + + return hit; + }); + + return { events }; +}; diff --git a/x-pack/plugins/session_view/server/routes/index.ts b/x-pack/plugins/session_view/server/routes/index.ts index 7b9cfb45f580b..b07a416fc05bd 100644 --- a/x-pack/plugins/session_view/server/routes/index.ts +++ b/x-pack/plugins/session_view/server/routes/index.ts @@ -6,9 +6,11 @@ */ import { IRouter } from '../../../../../src/core/server'; import { registerProcessEventsRoute } from './process_events_route'; +import { registerAlertsRoute } from './alerts_route'; import { sessionEntryLeadersRoute } from './session_entry_leaders_route'; export const registerRoutes = (router: IRouter) => { registerProcessEventsRoute(router); sessionEntryLeadersRoute(router); + registerAlertsRoute(router); }; diff --git a/x-pack/plugins/session_view/server/routes/process_events_route.ts b/x-pack/plugins/session_view/server/routes/process_events_route.ts index 47e2d917733d5..524bace430adf 100644 --- a/x-pack/plugins/session_view/server/routes/process_events_route.ts +++ b/x-pack/plugins/session_view/server/routes/process_events_route.ts @@ -11,10 +11,8 @@ import { PROCESS_EVENTS_ROUTE, PROCESS_EVENTS_PER_PAGE, PROCESS_EVENTS_INDEX, - ALERTS_INDEX, ENTRY_SESSION_ENTITY_ID_PROPERTY, } from '../../common/constants'; -import { expandDottedObject } from '../../common/utils/expand_dotted_object'; export const registerProcessEventsRoute = (router: IRouter) => { router.get( @@ -45,9 +43,7 @@ export const doSearch = async ( forward = true ) => { const search = await client.search({ - // TODO: move alerts into it's own route with it's own pagination. - index: [PROCESS_EVENTS_INDEX, ALERTS_INDEX], - ignore_unavailable: true, + index: [PROCESS_EVENTS_INDEX], body: { query: { match: { @@ -67,13 +63,7 @@ export const doSearch = async ( }, }); - const events = search.hits.hits.map((hit: any) => { - // TODO: re-eval if this is needed after moving alerts to it's own route. - // the .siem-signals-default index flattens many properties. this util unflattens them. - hit._source = expandDottedObject(hit._source); - - return hit; - }); + const events = search.hits.hits; if (!forward) { events.reverse(); From ab7b6d3a62f6a222e0b00a6f766023f64d9e4da2 Mon Sep 17 00:00:00 2001 From: mitodrummer Date: Tue, 8 Mar 2022 22:10:22 -0800 Subject: [PATCH 02/13] View mode toggle + group view implemented --- .../detail_panel_alert_actions/index.test.tsx | 61 ++++++++++ .../detail_panel_alert_actions/index.tsx | 89 +++++++++++++++ .../detail_panel_alert_actions/styles.ts | 107 ++++++++++++++++++ .../index.test.tsx | 61 ++++++++++ .../detail_panel_alert_group_item/index.tsx | 72 ++++++++++++ .../detail_panel_alert_list_item/index.tsx | 105 +++++++---------- .../detail_panel_alert_list_item/styles.ts | 21 +++- .../detail_panel_alert_tab/index.tsx | 81 +++++++++++-- .../detail_panel_alert_tab/styles.ts | 6 + 9 files changed, 523 insertions(+), 80 deletions(-) create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_actions/styles.ts create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.test.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx new file mode 100644 index 0000000000000..e6572a097d85a --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { DetailPanelListItem } from './index'; + +const TEST_STRING = 'item title'; +const TEST_CHILD = {TEST_STRING}; +const TEST_COPY_STRING = 'test copy button'; +const BUTTON_TEST_ID = 'sessionView:test-copy-button'; +const TEST_COPY = ; +const LIST_ITEM_TEST_ID = 'sessionView:detail-panel-list-item'; +const WAIT_TIMEOUT = 500; + +describe('DetailPanelListItem component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When DetailPanelListItem is mounted', () => { + it('renders DetailPanelListItem correctly', async () => { + renderResult = mockedContext.render({TEST_CHILD}); + + expect(renderResult.queryByTestId(LIST_ITEM_TEST_ID)).toBeVisible(); + expect(renderResult.queryByText(TEST_STRING)).toBeVisible(); + }); + + it('renders copy element correctly', async () => { + renderResult = mockedContext.render( + {TEST_CHILD} + ); + + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + fireEvent.mouseEnter(renderResult.getByTestId(LIST_ITEM_TEST_ID)); + await waitFor(() => screen.queryByTestId(BUTTON_TEST_ID)); + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeVisible(); + + fireEvent.mouseLeave(renderResult.getByTestId(LIST_ITEM_TEST_ID)); + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + }); + + it('does not have mouse events when copy prop is not present', async () => { + renderResult = mockedContext.render({TEST_CHILD}); + + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + fireEvent.mouseEnter(renderResult.getByTestId(LIST_ITEM_TEST_ID)); + await waitFor(() => screen.queryByTestId(BUTTON_TEST_ID), { timeout: WAIT_TIMEOUT }); + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx new file mode 100644 index 0000000000000..7565507b808d2 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiPopover, EuiContextMenuPanel, EuiButtonIcon, EuiContextMenuItem } from '@elastic/eui'; +import { Process, ProcessEvent } from '../../../common/types/process_tree'; +import { ProcessImpl } from '../process_tree/hooks'; + +interface DetailPanelAlertActionsDeps { + event: ProcessEvent; + onProcessSelected: (process: Process) => void; +} + +/** + * Detail panel alert context menu actions + */ +export const DetailPanelAlertActions = ({ + event, + onProcessSelected, +}: DetailPanelAlertActionsDeps) => { + const [isPopoverOpen, setPopover] = useState(false); + + const onClosePopover = useCallback(() => { + setPopover(false); + }, []); + + const onToggleMenu = useCallback(() => { + setPopover(!isPopoverOpen); + }, [isPopoverOpen]); + + const onJumpToAlert = () => { + const process = new ProcessImpl(event.process.entity_id); + process.addEvent(event); + + onProcessSelected(process); + setPopover(false); + }; + + const onShowDetails = useCallback(() => { + // TODO: call into alert flyout + setPopover(false); + }, []); + + if (!event.kibana) { + return null; + } + + const { uuid } = event.kibana.alert; + + const menuItems = [ + + + , + + + , + ]; + + return ( + + } + isOpen={isPopoverOpen} + closePopover={onClosePopover} + panelPaddingSize="none" + anchorPosition="leftCenter" + > + + + ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/styles.ts new file mode 100644 index 0000000000000..14d0be374b5d1 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/styles.ts @@ -0,0 +1,107 @@ +/* + * 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 { useMemo } from 'react'; +import { useEuiTheme, transparentize } from '@elastic/eui'; +import { CSSObject, css } from '@emotion/react'; + +interface StylesDeps { + minimal?: boolean; + isInvestigated?: boolean; +} + +export const useStyles = ({ minimal = false, isInvestigated = false }: StylesDeps) => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const { colors, font, size, border } = euiTheme; + + const dangerBorder = transparentize(colors.danger, 0.2); + const dangerBackground = transparentize(colors.danger, 0.08); + const borderThickness = border.width.thin; + const mediumPadding = size.m; + + let alertTitleColor = colors.text; + let borderColor = colors.lightShade; + + if (isInvestigated) { + alertTitleColor = colors.primaryText; + borderColor = dangerBorder; + } + + const alertItem = css` + border: ${borderThickness} solid ${borderColor}; + padding: ${mediumPadding}; + border-radius: ${border.radius.medium}; + + margin: 0 ${mediumPadding} ${mediumPadding} ${mediumPadding}; + background-color: ${colors.emptyShade}; + + & .euiAccordion__buttonContent { + width: 100%; + } + + & .euiAccordion__button { + min-width: 0; + width: calc(100% - ${size.l}); + } + + & .euiAccordion__childWrapper { + overflow: visible; + } + `; + + const alertTitle: CSSObject = { + display: minimal ? 'none' : 'initial', + color: alertTitleColor, + fontWeight: font.weight.semiBold, + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + }; + + const alertIcon: CSSObject = { + marginRight: size.s, + }; + + const alertAccordionButton: CSSObject = { + width: `calc(100% - ${size.l})`, + minWidth: 0, + }; + + const processPanel: CSSObject = { + border: `${borderThickness} solid ${colors.lightShade}`, + fontFamily: font.familyCode, + marginTop: mediumPadding, + padding: `${size.xs} ${size.s}`, + }; + + const investigatedLabel: CSSObject = { + position: 'relative', + zIndex: 1, + bottom: `-${mediumPadding}`, + left: `-${mediumPadding}`, + width: `calc(100% + ${mediumPadding} * 2)`, + borderTop: `${borderThickness} solid ${dangerBorder}`, + borderBottomLeftRadius: border.radius.medium, + borderBottomRightRadius: border.radius.medium, + backgroundColor: dangerBackground, + textAlign: 'center', + }; + + return { + alertItem, + alertTitle, + alertIcon, + alertAccordionButton, + processPanel, + investigatedLabel, + }; + }, [euiTheme, isInvestigated, minimal]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.test.tsx new file mode 100644 index 0000000000000..e6572a097d85a --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.test.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { DetailPanelListItem } from './index'; + +const TEST_STRING = 'item title'; +const TEST_CHILD = {TEST_STRING}; +const TEST_COPY_STRING = 'test copy button'; +const BUTTON_TEST_ID = 'sessionView:test-copy-button'; +const TEST_COPY = ; +const LIST_ITEM_TEST_ID = 'sessionView:detail-panel-list-item'; +const WAIT_TIMEOUT = 500; + +describe('DetailPanelListItem component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When DetailPanelListItem is mounted', () => { + it('renders DetailPanelListItem correctly', async () => { + renderResult = mockedContext.render({TEST_CHILD}); + + expect(renderResult.queryByTestId(LIST_ITEM_TEST_ID)).toBeVisible(); + expect(renderResult.queryByText(TEST_STRING)).toBeVisible(); + }); + + it('renders copy element correctly', async () => { + renderResult = mockedContext.render( + {TEST_CHILD} + ); + + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + fireEvent.mouseEnter(renderResult.getByTestId(LIST_ITEM_TEST_ID)); + await waitFor(() => screen.queryByTestId(BUTTON_TEST_ID)); + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeVisible(); + + fireEvent.mouseLeave(renderResult.getByTestId(LIST_ITEM_TEST_ID)); + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + }); + + it('does not have mouse events when copy prop is not present', async () => { + renderResult = mockedContext.render({TEST_CHILD}); + + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + fireEvent.mouseEnter(renderResult.getByTestId(LIST_ITEM_TEST_ID)); + await waitFor(() => screen.queryByTestId(BUTTON_TEST_ID), { timeout: WAIT_TIMEOUT }); + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx new file mode 100644 index 0000000000000..df20216120379 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useMemo } from 'react'; +import { EuiIcon, EuiText, EuiAccordion, EuiNotificationBadge } from '@elastic/eui'; +import { Process, ProcessEvent } from '../../../common/types/process_tree'; +import { useStyles } from '../detail_panel_alert_list_item/styles'; +import { DetailPanelAlertListItem } from '../detail_panel_alert_list_item'; +import { ALERT_COUNT_THRESHOLD } from '../../../common/constants'; + +interface DetailPanelAlertsGroupItemDeps { + alerts: ProcessEvent[]; + onProcessSelected: (process: Process) => void; +} + +/** + * Detail panel description list item. + */ +export const DetailPanelAlertGroupItem = ({ + alerts, + onProcessSelected, +}: DetailPanelAlertsGroupItemDeps) => { + const styles = useStyles({}); + + const alertsCount = useMemo(() => { + return alerts.length >= ALERT_COUNT_THRESHOLD ? ALERT_COUNT_THRESHOLD + '+' : alerts.length; + }, [alerts]); + + if (!alerts[0].kibana) { + return null; + } + + const { rule } = alerts[0].kibana.alert; + + return ( + +

+ + {rule.name} +

+ + } + css={styles.alertItem} + extraAction={ + + {alertsCount} + + } + > + {alerts.map((event) => { + const key = 'minimal_' + event.kibana?.alert.uuid; + + return ( + + ); + })} +
+ ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx index 1e8aaa44fa04b..1d44d2ff6148a 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx @@ -4,27 +4,27 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState, useCallback } from 'react'; +import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { + EuiFlexItem, + EuiFlexGroup, EuiSpacer, EuiIcon, EuiText, EuiAccordion, EuiPanel, - EuiPopover, - EuiContextMenuPanel, - EuiButtonIcon, - EuiContextMenuItem, + EuiHorizontalRule, } from '@elastic/eui'; import { Process, ProcessEvent } from '../../../common/types/process_tree'; -import { ProcessImpl } from '../process_tree/hooks'; import { useStyles } from './styles'; +import { DetailPanelAlertActions } from '../detail_panel_alert_actions'; interface DetailPanelAlertsListItemDeps { event: ProcessEvent; onProcessSelected: (process: Process) => void; isInvestigated?: boolean; + minimal?: boolean; } /** @@ -33,31 +33,10 @@ interface DetailPanelAlertsListItemDeps { export const DetailPanelAlertListItem = ({ event, onProcessSelected, - isInvestigated = false, + isInvestigated, + minimal, }: DetailPanelAlertsListItemDeps) => { - const styles = useStyles({ isInvestigated }); - const [isPopoverOpen, setPopover] = useState(false); - - const onClosePopover = useCallback(() => { - setPopover(false); - }, []); - - const onToggleMenu = useCallback(() => { - setPopover(!isPopoverOpen); - }, [isPopoverOpen]); - - const onJumpToAlert = () => { - const process = new ProcessImpl(event.process.entity_id); - process.addEvent(event); - - onProcessSelected(process); - setPopover(false); - }; - - const onShowDetails = useCallback(() => { - // TODO: call into alert flyout - setPopover(false); - }, []); + const styles = useStyles({ minimal, isInvestigated }); if (!event.kibana) { return null; @@ -67,24 +46,37 @@ export const DetailPanelAlertListItem = ({ const { uuid, name } = event.kibana.alert.rule; const { args } = event.process; - const menuItems = [ - - - , - - - , - ]; - const forceState = !isInvestigated ? 'open' : undefined; - return ( + return minimal ? ( + <> + + + + + {timestamp} + + + + + + + + {args.join(' ')} + + + + ) : ( - } - isOpen={isPopoverOpen} - closePopover={onClosePopover} - panelPaddingSize="none" - anchorPosition="leftCenter" - > - - - } + extraAction={} > diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts index fa8267e81ebc4..d0bc1719201bd 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts @@ -10,10 +10,11 @@ import { useEuiTheme, transparentize } from '@elastic/eui'; import { CSSObject, css } from '@emotion/react'; interface StylesDeps { - isInvestigated: boolean; + minimal?: boolean; + isInvestigated?: boolean; } -export const useStyles = ({ isInvestigated }: StylesDeps) => { +export const useStyles = ({ minimal = false, isInvestigated = false }: StylesDeps) => { const { euiTheme } = useEuiTheme(); const cached = useMemo(() => { @@ -36,6 +37,7 @@ export const useStyles = ({ isInvestigated }: StylesDeps) => { border: ${borderThickness} solid ${borderColor}; padding: ${mediumPadding}; border-radius: ${border.radius.medium}; + margin: 0 ${mediumPadding} ${mediumPadding} ${mediumPadding}; background-color: ${colors.emptyShade}; @@ -54,6 +56,7 @@ export const useStyles = ({ isInvestigated }: StylesDeps) => { `; const alertTitle: CSSObject = { + display: minimal ? 'none' : 'initial', color: alertTitleColor, fontWeight: font.weight.semiBold, textOverflow: 'ellipsis', @@ -73,7 +76,7 @@ export const useStyles = ({ isInvestigated }: StylesDeps) => { const processPanel: CSSObject = { border: `${borderThickness} solid ${colors.lightShade}`, fontFamily: font.familyCode, - marginTop: mediumPadding, + marginTop: minimal ? size.s : size.m, padding: `${size.xs} ${size.s}`, }; @@ -90,6 +93,14 @@ export const useStyles = ({ isInvestigated }: StylesDeps) => { textAlign: 'center', }; + const minimalContextMenu: CSSObject = { + float: 'right', + }; + + const minimalHR: CSSObject = { + marginBottom: 0, + }; + return { alertItem, alertTitle, @@ -97,8 +108,10 @@ export const useStyles = ({ isInvestigated }: StylesDeps) => { alertAccordionButton, processPanel, investigatedLabel, + minimalContextMenu, + minimalHR, }; - }, [euiTheme, isInvestigated]); + }, [euiTheme, isInvestigated, minimal]); return cached; }; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx index 797f8bf904638..200097699f99b 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx @@ -4,11 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; -import { EuiHorizontalRule } from '@elastic/eui'; +import React, { useState, useMemo } from 'react'; +import { EuiButtonGroup, EuiHorizontalRule } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { groupBy } from 'lodash'; import { ProcessEvent, Process } from '../../../common/types/process_tree'; import { useStyles } from './styles'; import { DetailPanelAlertListItem } from '../detail_panel_alert_list_item'; +import { DetailPanelAlertGroupItem } from '../detail_panel_alert_group_item'; interface DetailPanelAlertTabDeps { alerts: ProcessEvent[]; @@ -16,6 +19,13 @@ interface DetailPanelAlertTabDeps { investigatedAlert?: ProcessEvent; } +const VIEW_MODE_LIST = 'listView'; +const VIEW_MODE_GROUP = 'groupView'; + +export type AlertsGroup = { + [key: string]: ProcessEvent[]; +}; + /** * Host Panel of session view detail panel. */ @@ -25,11 +35,48 @@ export const DetailPanelAlertTab = ({ investigatedAlert, }: DetailPanelAlertTabDeps) => { const styles = useStyles(); + const [viewMode, setViewMode] = useState(VIEW_MODE_LIST); + const viewModes = [ + { + id: VIEW_MODE_LIST, + label: i18n.translate('xpack.sessionView.alertDetailsTab.listView', { + defaultMessage: 'List view', + }), + }, + { + id: VIEW_MODE_GROUP, + label: i18n.translate('xpack.sessionView.alertDetailsTab.groupView', { + defaultMessage: 'Group view', + }), + }, + ]; + const filteredAlerts = useMemo(() => { + return alerts.filter((event) => { + const isInvestigatedAlert = + event.kibana?.alert.uuid === investigatedAlert?.kibana?.alert.uuid; + return !isInvestigatedAlert; + }); + }, [investigatedAlert, alerts]); + + const groupedAlerts = useMemo(() => { + return groupBy(filteredAlerts, (event) => event.kibana?.alert.rule.uuid); + }, [filteredAlerts]); + + // TODO: testing investigatedAlert = alerts[0]; return (
+ {investigatedAlert && (
)} - {alerts.map((event) => { - const isInvestigatedAlert = - event.kibana?.alert.uuid === investigatedAlert?.kibana?.alert.uuid; - if (isInvestigatedAlert) { - return null; - } + {viewMode === VIEW_MODE_LIST + ? filteredAlerts.map((event) => { + const key = event.kibana?.alert.uuid; + + return ( + + ); + }) + : Object.keys(groupedAlerts).map((ruleId: string) => { + const alertsByRule = groupedAlerts[ruleId]; - return ; - })} + return ( + + ); + })}
); }; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts index a4a85c33b258c..a906744cdafb2 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts @@ -27,9 +27,15 @@ export const useStyles = () => { paddingTop: size.base, }; + const viewMode: CSSObject = { + margin: size.base, + marginBottom: 0, + }; + return { container, stickyItem, + viewMode, }; }, [euiTheme]); From d74d24a56f36ba432a91bc01ec6a13227e4499bb Mon Sep 17 00:00:00 2001 From: mitodrummer Date: Wed, 9 Mar 2022 22:12:24 -0800 Subject: [PATCH 03/13] tests written --- .../detail_panel_alert_actions/index.test.tsx | 86 ++++-- .../detail_panel_alert_actions/index.tsx | 20 +- .../index.test.tsx | 61 ---- .../detail_panel_alert_group_item/index.tsx | 17 +- .../index.test.tsx | 61 ---- .../detail_panel_alert_list_item/index.tsx | 27 +- .../detail_panel_alert_tab/index.test.tsx | 281 +++++++++++++----- .../detail_panel_alert_tab/index.tsx | 12 +- .../components/process_tree/index.test.tsx | 5 +- .../public/components/session_view/index.tsx | 6 + .../session_view_detail_panel/index.test.tsx | 48 ++- .../session_view_detail_panel/index.tsx | 50 +++- .../server/routes/alerts_route.test.ts | 4 +- 13 files changed, 424 insertions(+), 254 deletions(-) delete mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.test.tsx delete mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.test.tsx diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx index e6572a097d85a..4ef537d89cfd4 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx @@ -6,56 +6,82 @@ */ import React from 'react'; -import { screen, fireEvent, waitFor } from '@testing-library/react'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; -import { DetailPanelListItem } from './index'; +import { DetailPanelAlertActions } from './index'; +import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock'; +import userEvent from '@testing-library/user-event'; +import { ProcessImpl } from '../process_tree/hooks'; -const TEST_STRING = 'item title'; -const TEST_CHILD = {TEST_STRING}; -const TEST_COPY_STRING = 'test copy button'; -const BUTTON_TEST_ID = 'sessionView:test-copy-button'; -const TEST_COPY = ; -const LIST_ITEM_TEST_ID = 'sessionView:detail-panel-list-item'; -const WAIT_TIMEOUT = 500; +export const BUTTON_TEST_ID = 'sessionView:detailPanelAlertActionsBtn'; +export const SHOW_DETAILS_TEST_ID = 'sessionView:detailPanelAlertActionShowDetails'; +export const JUMP_TO_PROCESS_TEST_ID = 'sessionView:detailPanelAlertActionJumpToProcess'; -describe('DetailPanelListItem component', () => { +describe('DetailPanelAlertActions component', () => { let render: () => ReturnType; let renderResult: ReturnType; let mockedContext: AppContextTestRender; + let mockShowAlertDetails = jest.fn((uuid) => uuid); + let mockOnProcessSelected = jest.fn((process) => process); beforeEach(() => { mockedContext = createAppRootMockRenderer(); + mockShowAlertDetails = jest.fn((uuid) => uuid); + mockOnProcessSelected = jest.fn((process) => process); }); - describe('When DetailPanelListItem is mounted', () => { - it('renders DetailPanelListItem correctly', async () => { - renderResult = mockedContext.render({TEST_CHILD}); + describe('When DetailPanelAlertActions is mounted', () => { + it('renders a popover when button is clicked', async () => { + const mockEvent = mockAlerts[0]; - expect(renderResult.queryByTestId(LIST_ITEM_TEST_ID)).toBeVisible(); - expect(renderResult.queryByText(TEST_STRING)).toBeVisible(); + renderResult = mockedContext.render( + + ); + + userEvent.click(renderResult.getByTestId(BUTTON_TEST_ID)); + expect(renderResult.queryByTestId(SHOW_DETAILS_TEST_ID)).toBeTruthy(); + expect(renderResult.queryByTestId(JUMP_TO_PROCESS_TEST_ID)).toBeTruthy(); + expect(mockShowAlertDetails.mock.calls.length).toBe(0); + expect(mockOnProcessSelected.mock.calls.length).toBe(0); }); - it('renders copy element correctly', async () => { + it('calls alert flyout callback when View details clicked', async () => { + const mockEvent = mockAlerts[0]; + renderResult = mockedContext.render( - {TEST_CHILD} + ); - expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); - fireEvent.mouseEnter(renderResult.getByTestId(LIST_ITEM_TEST_ID)); - await waitFor(() => screen.queryByTestId(BUTTON_TEST_ID)); - expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeVisible(); - - fireEvent.mouseLeave(renderResult.getByTestId(LIST_ITEM_TEST_ID)); - expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + userEvent.click(renderResult.getByTestId(BUTTON_TEST_ID)); + userEvent.click(renderResult.getByTestId(SHOW_DETAILS_TEST_ID)); + expect(mockShowAlertDetails.mock.calls.length).toBe(1); + expect(mockShowAlertDetails.mock.results[0].value).toBe(mockEvent.kibana?.alert.uuid); + expect(mockOnProcessSelected.mock.calls.length).toBe(0); }); - it('does not have mouse events when copy prop is not present', async () => { - renderResult = mockedContext.render({TEST_CHILD}); + it('calls onProcessSelected when Jump to process clicked', async () => { + const mockEvent = mockAlerts[0]; + + renderResult = mockedContext.render( + + ); - expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); - fireEvent.mouseEnter(renderResult.getByTestId(LIST_ITEM_TEST_ID)); - await waitFor(() => screen.queryByTestId(BUTTON_TEST_ID), { timeout: WAIT_TIMEOUT }); - expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); + userEvent.click(renderResult.getByTestId(BUTTON_TEST_ID)); + userEvent.click(renderResult.getByTestId(JUMP_TO_PROCESS_TEST_ID)); + expect(mockOnProcessSelected.mock.calls.length).toBe(1); + expect(mockOnProcessSelected.mock.results[0].value).toBeInstanceOf(ProcessImpl); + expect(mockShowAlertDetails.mock.calls.length).toBe(0); }); }); }); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx index 7565507b808d2..006a5dccf6f83 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx @@ -9,9 +9,11 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiPopover, EuiContextMenuPanel, EuiButtonIcon, EuiContextMenuItem } from '@elastic/eui'; import { Process, ProcessEvent } from '../../../common/types/process_tree'; import { ProcessImpl } from '../process_tree/hooks'; +import { BUTTON_TEST_ID, SHOW_DETAILS_TEST_ID, JUMP_TO_PROCESS_TEST_ID } from './index.test'; interface DetailPanelAlertActionsDeps { event: ProcessEvent; + onShowAlertDetails: (alertId: string) => void; onProcessSelected: (process: Process) => void; } @@ -20,6 +22,7 @@ interface DetailPanelAlertActionsDeps { */ export const DetailPanelAlertActions = ({ event, + onShowAlertDetails, onProcessSelected, }: DetailPanelAlertActionsDeps) => { const [isPopoverOpen, setPopover] = useState(false); @@ -41,9 +44,11 @@ export const DetailPanelAlertActions = ({ }; const onShowDetails = useCallback(() => { - // TODO: call into alert flyout - setPopover(false); - }, []); + if (event.kibana) { + onShowAlertDetails(event.kibana.alert.uuid); + setPopover(false); + } + }, [event, onShowAlertDetails]); if (!event.kibana) { return null; @@ -52,13 +57,17 @@ export const DetailPanelAlertActions = ({ const { uuid } = event.kibana.alert; const menuItems = [ - + , - + } diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.test.tsx deleted file mode 100644 index e6572a097d85a..0000000000000 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.test.tsx +++ /dev/null @@ -1,61 +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 { screen, fireEvent, waitFor } from '@testing-library/react'; -import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; -import { DetailPanelListItem } from './index'; - -const TEST_STRING = 'item title'; -const TEST_CHILD = {TEST_STRING}; -const TEST_COPY_STRING = 'test copy button'; -const BUTTON_TEST_ID = 'sessionView:test-copy-button'; -const TEST_COPY = ; -const LIST_ITEM_TEST_ID = 'sessionView:detail-panel-list-item'; -const WAIT_TIMEOUT = 500; - -describe('DetailPanelListItem component', () => { - let render: () => ReturnType; - let renderResult: ReturnType; - let mockedContext: AppContextTestRender; - - beforeEach(() => { - mockedContext = createAppRootMockRenderer(); - }); - - describe('When DetailPanelListItem is mounted', () => { - it('renders DetailPanelListItem correctly', async () => { - renderResult = mockedContext.render({TEST_CHILD}); - - expect(renderResult.queryByTestId(LIST_ITEM_TEST_ID)).toBeVisible(); - expect(renderResult.queryByText(TEST_STRING)).toBeVisible(); - }); - - it('renders copy element correctly', async () => { - renderResult = mockedContext.render( - {TEST_CHILD} - ); - - expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); - fireEvent.mouseEnter(renderResult.getByTestId(LIST_ITEM_TEST_ID)); - await waitFor(() => screen.queryByTestId(BUTTON_TEST_ID)); - expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeVisible(); - - fireEvent.mouseLeave(renderResult.getByTestId(LIST_ITEM_TEST_ID)); - expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); - }); - - it('does not have mouse events when copy prop is not present', async () => { - renderResult = mockedContext.render({TEST_CHILD}); - - expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); - fireEvent.mouseEnter(renderResult.getByTestId(LIST_ITEM_TEST_ID)); - await waitFor(() => screen.queryByTestId(BUTTON_TEST_ID), { timeout: WAIT_TIMEOUT }); - expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); - }); - }); -}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx index df20216120379..731e0345c916f 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx @@ -10,10 +10,16 @@ import { Process, ProcessEvent } from '../../../common/types/process_tree'; import { useStyles } from '../detail_panel_alert_list_item/styles'; import { DetailPanelAlertListItem } from '../detail_panel_alert_list_item'; import { ALERT_COUNT_THRESHOLD } from '../../../common/constants'; +import { + ALERT_GROUP_ITEM_TEST_ID, + ALERT_GROUP_ITEM_TITLE, + ALERT_GROUP_ITEM_COUNT, +} from '../detail_panel_alert_tab/index.test'; interface DetailPanelAlertsGroupItemDeps { alerts: ProcessEvent[]; onProcessSelected: (process: Process) => void; + onShowAlertDetails: (alertId: string) => void; } /** @@ -22,6 +28,7 @@ interface DetailPanelAlertsGroupItemDeps { export const DetailPanelAlertGroupItem = ({ alerts, onProcessSelected, + onShowAlertDetails, }: DetailPanelAlertsGroupItemDeps) => { const styles = useStyles({}); @@ -38,10 +45,11 @@ export const DetailPanelAlertGroupItem = ({ return ( +

{rule.name} @@ -50,7 +58,11 @@ export const DetailPanelAlertGroupItem = ({ } css={styles.alertItem} extraAction={ - + {alertsCount} } @@ -64,6 +76,7 @@ export const DetailPanelAlertGroupItem = ({ minimal event={event} onProcessSelected={onProcessSelected} + onShowAlertDetails={onShowAlertDetails} /> ); })} diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.test.tsx deleted file mode 100644 index e6572a097d85a..0000000000000 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.test.tsx +++ /dev/null @@ -1,61 +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 { screen, fireEvent, waitFor } from '@testing-library/react'; -import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; -import { DetailPanelListItem } from './index'; - -const TEST_STRING = 'item title'; -const TEST_CHILD = {TEST_STRING}; -const TEST_COPY_STRING = 'test copy button'; -const BUTTON_TEST_ID = 'sessionView:test-copy-button'; -const TEST_COPY = ; -const LIST_ITEM_TEST_ID = 'sessionView:detail-panel-list-item'; -const WAIT_TIMEOUT = 500; - -describe('DetailPanelListItem component', () => { - let render: () => ReturnType; - let renderResult: ReturnType; - let mockedContext: AppContextTestRender; - - beforeEach(() => { - mockedContext = createAppRootMockRenderer(); - }); - - describe('When DetailPanelListItem is mounted', () => { - it('renders DetailPanelListItem correctly', async () => { - renderResult = mockedContext.render({TEST_CHILD}); - - expect(renderResult.queryByTestId(LIST_ITEM_TEST_ID)).toBeVisible(); - expect(renderResult.queryByText(TEST_STRING)).toBeVisible(); - }); - - it('renders copy element correctly', async () => { - renderResult = mockedContext.render( - {TEST_CHILD} - ); - - expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); - fireEvent.mouseEnter(renderResult.getByTestId(LIST_ITEM_TEST_ID)); - await waitFor(() => screen.queryByTestId(BUTTON_TEST_ID)); - expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeVisible(); - - fireEvent.mouseLeave(renderResult.getByTestId(LIST_ITEM_TEST_ID)); - expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); - }); - - it('does not have mouse events when copy prop is not present', async () => { - renderResult = mockedContext.render({TEST_CHILD}); - - expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); - fireEvent.mouseEnter(renderResult.getByTestId(LIST_ITEM_TEST_ID)); - await waitFor(() => screen.queryByTestId(BUTTON_TEST_ID), { timeout: WAIT_TIMEOUT }); - expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeNull(); - }); - }); -}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx index 1d44d2ff6148a..48583f6787e34 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx @@ -19,9 +19,15 @@ import { import { Process, ProcessEvent } from '../../../common/types/process_tree'; import { useStyles } from './styles'; import { DetailPanelAlertActions } from '../detail_panel_alert_actions'; +import { + ALERT_LIST_ITEM_TEST_ID, + ALERT_LIST_ITEM_TIMESTAMP, + ALERT_LIST_ITEM_ARGS, +} from '../detail_panel_alert_tab/index.test'; interface DetailPanelAlertsListItemDeps { event: ProcessEvent; + onShowAlertDetails: (alertId: string) => void; onProcessSelected: (process: Process) => void; isInvestigated?: boolean; minimal?: boolean; @@ -33,6 +39,7 @@ interface DetailPanelAlertsListItemDeps { export const DetailPanelAlertListItem = ({ event, onProcessSelected, + onShowAlertDetails, isInvestigated, minimal, }: DetailPanelAlertsListItemDeps) => { @@ -49,7 +56,7 @@ export const DetailPanelAlertListItem = ({ const forceState = !isInvestigated ? 'open' : undefined; return minimal ? ( - <> +

@@ -62,6 +69,7 @@ export const DetailPanelAlertListItem = ({ css={styles.minimalContextMenu} event={event} onProcessSelected={onProcessSelected} + onShowAlertDetails={onShowAlertDetails} /> @@ -75,10 +83,11 @@ export const DetailPanelAlertListItem = ({ {args.join(' ')} - +
) : ( @@ -91,10 +100,16 @@ export const DetailPanelAlertListItem = ({ initialIsOpen={true} forceState={forceState} css={styles.alertItem} - extraAction={} + extraAction={ + + } > - + {timestamp} - {args.join(' ')} + + {args.join(' ')} + {isInvestigated && (
diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx index 2df9f47e5a416..737a04b802090 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx @@ -6,83 +6,228 @@ */ import React from 'react'; + import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; -import { ProcessEventHost } from '../../../common/types/process_tree'; -import { DetailPanelHostTab } from './index'; - -const TEST_ARCHITECTURE = 'x86_64'; -const TEST_HOSTNAME = 'host-james-fleet-714-2'; -const TEST_ID = '48c1b3f1ac5da4e0057fc9f60f4d1d5d'; -const TEST_IP = '127.0.0.1,::1,10.132.0.50,fe80::7d39:3147:4d9a:f809'; -const TEST_MAC = '42:01:0a:84:00:32'; -const TEST_NAME = 'name-james-fleet-714-2'; -const TEST_OS_FAMILY = 'family-centos'; -const TEST_OS_FULL = 'full-CentOS 7.9.2009'; -const TEST_OS_KERNEL = '3.10.0-1160.31.1.el7.x86_64 #1 SMP Thu Jun 10 13:32:12 UTC 2021'; -const TEST_OS_NAME = 'os-Linux'; -const TEST_OS_PLATFORM = 'platform-centos'; -const TEST_OS_VERSION = 'version-7.9.2009'; - -const TEST_HOST: ProcessEventHost = { - architecture: TEST_ARCHITECTURE, - hostname: TEST_HOSTNAME, - id: TEST_ID, - ip: TEST_IP, - mac: TEST_MAC, - name: TEST_NAME, - os: { - family: TEST_OS_FAMILY, - full: TEST_OS_FULL, - kernel: TEST_OS_KERNEL, - name: TEST_OS_NAME, - platform: TEST_OS_PLATFORM, - version: TEST_OS_VERSION, - }, -}; - -describe('DetailPanelHostTab component', () => { +import { DetailPanelAlertTab } from './index'; +import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock'; +import { fireEvent } from '@testing-library/dom'; + +export const ALERT_LIST_ITEM_TEST_ID = 'sessionView:detailPanelAlertListItem'; +export const ALERT_GROUP_ITEM_TEST_ID = 'sessionView:detailPanelAlertGroupItem'; +export const INVESTIGATED_ALERT_TEST_ID = 'sessionView:detailPanelInvestigatedAlert'; +export const VIEW_MODE_TOGGLE = 'sessionView:detailPanelAlertsViewMode'; +export const ALERT_LIST_ITEM_TIMESTAMP = 'sessionView:detailPanelAlertListItemTimestamp'; +export const ALERT_LIST_ITEM_ARGS = 'sessionView:detailPanelAlertListItemArgs'; +export const ALERT_GROUP_ITEM_COUNT = 'sessionView:detailPanelAlertGroupCount'; +export const ALERT_GROUP_ITEM_TITLE = 'sessionView:detailPanelAlertGroupTitle'; + +const ACCORDION_BUTTON_CLASS = '.euiAccordion__button'; +const VIEW_MODE_GROUP = 'groupView'; +const ARIA_EXPANDED_ATTR = 'aria-expanded'; + +describe('DetailPanelAlertTab component', () => { let render: () => ReturnType; let renderResult: ReturnType; let mockedContext: AppContextTestRender; + let mockOnProcessSelected = jest.fn((process) => process); + let mockShowAlertDetails = jest.fn((alertId) => alertId); beforeEach(() => { mockedContext = createAppRootMockRenderer(); + mockOnProcessSelected = jest.fn((process) => process); + mockShowAlertDetails = jest.fn((alertId) => alertId); }); - describe('When DetailPanelHostTab is mounted', () => { - it('renders DetailPanelHostTab correctly', async () => { - renderResult = mockedContext.render(); - - expect(renderResult.queryByText('architecture')).toBeVisible(); - expect(renderResult.queryByText('hostname')).toBeVisible(); - expect(renderResult.queryByText('id')).toBeVisible(); - expect(renderResult.queryByText('ip')).toBeVisible(); - expect(renderResult.queryByText('mac')).toBeVisible(); - expect(renderResult.queryByText('name')).toBeVisible(); - expect(renderResult.queryByText(TEST_ARCHITECTURE)).toBeVisible(); - expect(renderResult.queryByText(TEST_HOSTNAME)).toBeVisible(); - expect(renderResult.queryByText(TEST_ID)).toBeVisible(); - expect(renderResult.queryByText(TEST_IP)).toBeVisible(); - expect(renderResult.queryByText(TEST_MAC)).toBeVisible(); - expect(renderResult.queryByText(TEST_NAME)).toBeVisible(); - - // expand host os accordion - renderResult - .queryByTestId('sessionView:detail-panel-accordion') - ?.querySelector('button') - ?.click(); - expect(renderResult.queryByText('os.family')).toBeVisible(); - expect(renderResult.queryByText('os.full')).toBeVisible(); - expect(renderResult.queryByText('os.kernel')).toBeVisible(); - expect(renderResult.queryByText('os.name')).toBeVisible(); - expect(renderResult.queryByText('os.platform')).toBeVisible(); - expect(renderResult.queryByText('os.version')).toBeVisible(); - expect(renderResult.queryByText(TEST_OS_FAMILY)).toBeVisible(); - expect(renderResult.queryByText(TEST_OS_FULL)).toBeVisible(); - expect(renderResult.queryByText(TEST_OS_KERNEL)).toBeVisible(); - expect(renderResult.queryByText(TEST_OS_NAME)).toBeVisible(); - expect(renderResult.queryByText(TEST_OS_PLATFORM)).toBeVisible(); - expect(renderResult.queryByText(TEST_OS_VERSION)).toBeVisible(); + describe('When DetailPanelAlertTab is mounted', () => { + it('renders a list of alerts for the session (defaulting to list view mode)', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryAllByTestId(ALERT_LIST_ITEM_TEST_ID).length).toBe(mockAlerts.length); + expect(renderResult.queryByTestId(ALERT_GROUP_ITEM_TEST_ID)).toBeFalsy(); + expect(renderResult.queryByTestId(INVESTIGATED_ALERT_TEST_ID)).toBeFalsy(); + expect( + renderResult + .queryByTestId(VIEW_MODE_TOGGLE) + ?.querySelector('.euiButtonGroupButton-isSelected')?.textContent + ).toBe('List view'); + }); + + it('renders a list of alerts grouped by rule when group-view clicked', async () => { + renderResult = mockedContext.render( + + ); + + fireEvent.click(renderResult.getByTestId(VIEW_MODE_GROUP)); + + expect(renderResult.queryAllByTestId(ALERT_LIST_ITEM_TEST_ID).length).toBe(mockAlerts.length); + expect(renderResult.queryByTestId(ALERT_GROUP_ITEM_TEST_ID)).toBeTruthy(); + expect(renderResult.queryByTestId(INVESTIGATED_ALERT_TEST_ID)).toBeFalsy(); + expect( + renderResult + .queryByTestId(VIEW_MODE_TOGGLE) + ?.querySelector('.euiButtonGroupButton-isSelected')?.textContent + ).toBe('Group view'); + }); + + it('renders a sticky investigated alert (outside of main list) if one is set', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryByTestId(INVESTIGATED_ALERT_TEST_ID)).toBeTruthy(); + + fireEvent.click(renderResult.getByTestId(VIEW_MODE_GROUP)); + + expect(renderResult.queryByTestId(INVESTIGATED_ALERT_TEST_ID)).toBeTruthy(); + }); + + it('investigated alert should be collapsible', async () => { + renderResult = mockedContext.render( + + ); + + expect( + renderResult + .queryByTestId(INVESTIGATED_ALERT_TEST_ID) + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('true'); + + const expandButton = renderResult + .queryByTestId(INVESTIGATED_ALERT_TEST_ID) + ?.querySelector(ACCORDION_BUTTON_CLASS); + + if (expandButton) { + fireEvent.click(expandButton); + } + + expect( + renderResult + .queryByTestId(INVESTIGATED_ALERT_TEST_ID) + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('false'); + }); + + it('non investigated alert should NOT be collapsible', async () => { + renderResult = mockedContext.render( + + ); + + expect( + renderResult + .queryAllByTestId(ALERT_LIST_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('true'); + + const expandButton = renderResult + .queryAllByTestId(ALERT_LIST_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS); + + if (expandButton) { + fireEvent.click(expandButton); + } + + expect( + renderResult + .queryAllByTestId(ALERT_LIST_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('true'); + }); + + it('grouped alerts should be expandable/collapsible (default to collapsed)', async () => { + renderResult = mockedContext.render( + + ); + + fireEvent.click(renderResult.getByTestId(VIEW_MODE_GROUP)); + + expect( + renderResult + .queryAllByTestId(ALERT_GROUP_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('false'); + + const expandButton = renderResult + .queryAllByTestId(ALERT_GROUP_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS); + + if (expandButton) { + fireEvent.click(expandButton); + } + + expect( + renderResult + .queryAllByTestId(ALERT_GROUP_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('true'); + }); + + it('each alert list item should show a timestamp and process arguments', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryAllByTestId(ALERT_LIST_ITEM_TIMESTAMP)[0]).toHaveTextContent( + mockAlerts[0]['@timestamp'] + ); + + expect(renderResult.queryAllByTestId(ALERT_LIST_ITEM_ARGS)[0]).toHaveTextContent( + mockAlerts[0].process.args.join(' ') + ); + }); + + it('each alert group should show a rule title and alert count', async () => { + renderResult = mockedContext.render( + + ); + + fireEvent.click(renderResult.getByTestId(VIEW_MODE_GROUP)); + + expect(renderResult.queryByTestId(ALERT_GROUP_ITEM_COUNT)).toHaveTextContent('2'); + expect(renderResult.queryByTestId(ALERT_GROUP_ITEM_TITLE)).toHaveTextContent( + mockAlerts[0].kibana?.alert.rule.name || '' + ); }); }); }); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx index 200097699f99b..298ba215f2360 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx @@ -12,10 +12,12 @@ import { ProcessEvent, Process } from '../../../common/types/process_tree'; import { useStyles } from './styles'; import { DetailPanelAlertListItem } from '../detail_panel_alert_list_item'; import { DetailPanelAlertGroupItem } from '../detail_panel_alert_group_item'; +import { INVESTIGATED_ALERT_TEST_ID, VIEW_MODE_TOGGLE } from './index.test'; interface DetailPanelAlertTabDeps { alerts: ProcessEvent[]; onProcessSelected: (process: Process) => void; + onShowAlertDetails: (alertId: string) => void; investigatedAlert?: ProcessEvent; } @@ -32,6 +34,7 @@ export type AlertsGroup = { export const DetailPanelAlertTab = ({ alerts, onProcessSelected, + onShowAlertDetails, investigatedAlert, }: DetailPanelAlertTabDeps) => { const styles = useStyles(); @@ -63,12 +66,10 @@ export const DetailPanelAlertTab = ({ return groupBy(filteredAlerts, (event) => event.kibana?.alert.rule.uuid); }, [filteredAlerts]); - // TODO: testing - investigatedAlert = alerts[0]; - return (
{investigatedAlert && ( -
+
@@ -97,6 +99,7 @@ export const DetailPanelAlertTab = ({ key={key} event={event} onProcessSelected={onProcessSelected} + onShowAlertDetails={onShowAlertDetails} /> ); }) @@ -108,6 +111,7 @@ export const DetailPanelAlertTab = ({ key={alertsByRule[0].kibana?.alert.rule.uuid} alerts={alertsByRule} onProcessSelected={onProcessSelected} + onShowAlertDetails={onShowAlertDetails} /> ); })} diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx index ac6807984ba83..94e2debe4d1bd 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { mockData } from '../../../common/mocks/constants/session_view_process.mock'; +import { mockData, mockAlerts } from '../../../common/mocks/constants/session_view_process.mock'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; import { ProcessImpl } from './hooks'; import { ProcessTree } from './index'; @@ -26,6 +26,7 @@ describe('ProcessTree component', () => { true} hasNextPage={false} @@ -45,6 +46,7 @@ describe('ProcessTree component', () => { true} hasNextPage={false} @@ -71,6 +73,7 @@ describe('ProcessTree component', () => { true} hasNextPage={false} diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx index be89398f7f885..eeeb5d0e2e1ef 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -41,6 +41,10 @@ export const SessionView = ({ sessionEntityId, height, jumpToEvent }: SessionVie setSelectedProcess(process); }, []); + const onShowAlertDetails = useCallback((alertId: string) => { + // TODO: hook into callback for alert flyout. + }, []); + const toggleDetailPanel = () => { setIsDetailOpen(!isDetailOpen); }; @@ -191,8 +195,10 @@ export const SessionView = ({ sessionEntityId, height, jumpToEvent }: SessionVie > diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx index f754086fe5fab..40e71efd8a6cf 100644 --- a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx @@ -6,7 +6,10 @@ */ import React from 'react'; -import { sessionViewBasicProcessMock } from '../../../common/mocks/constants/session_view_process.mock'; +import { + mockAlerts, + sessionViewBasicProcessMock, +} from '../../../common/mocks/constants/session_view_process.mock'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; import { SessionViewDetailPanel } from './index'; @@ -14,27 +17,66 @@ describe('SessionView component', () => { let render: () => ReturnType; let renderResult: ReturnType; let mockedContext: AppContextTestRender; + let mockOnProcessSelected = jest.fn((process) => process); + let mockShowAlertDetails = jest.fn((alertId) => alertId); beforeEach(() => { mockedContext = createAppRootMockRenderer(); + mockOnProcessSelected = jest.fn((process) => process); + mockShowAlertDetails = jest.fn((alertId) => alertId); }); describe('When SessionViewDetailPanel is mounted', () => { it('shows process detail by default', async () => { renderResult = mockedContext.render( - + ); expect(renderResult.queryByText('8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726')).toBeVisible(); }); it('can switch tabs to show host details', async () => { renderResult = mockedContext.render( - + ); renderResult.queryByText('Host')?.click(); expect(renderResult.queryByText('hostname')).toBeVisible(); expect(renderResult.queryAllByText('james-fleet-714-2')).toHaveLength(2); }); + + it('can switch tabs to show alert details', async () => { + renderResult = mockedContext.render( + + ); + + renderResult.queryByText('Alerts')?.click(); + expect(renderResult.queryByText('List view')).toBeVisible(); + }); + it('alert tab disabled when no alerts', async () => { + renderResult = mockedContext.render( + + ); + + renderResult.queryByText('Alerts')?.click(); + expect(renderResult.queryByText('List view')).toBeFalsy(); + }); }); }); diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx index 2ac1c37cbac44..5e0ddbce8fcfc 100644 --- a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx @@ -6,6 +6,7 @@ */ import React, { useState, useMemo, useCallback } from 'react'; import { EuiTabs, EuiTab, EuiNotificationBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { EuiTabProps } from '../../types'; import { Process, ProcessEvent } from '../../../common/types/process_tree'; import { getDetailPanelProcess, getSelectedTabContent } from './helpers'; @@ -15,9 +16,11 @@ import { DetailPanelAlertTab } from '../detail_panel_alert_tab'; import { ALERT_COUNT_THRESHOLD } from '../../../common/constants'; interface SessionViewDetailPanelDeps { - alerts: ProcessEvent[] | undefined; selectedProcess: Process; + alerts?: ProcessEvent[]; + investigatedAlert?: ProcessEvent; onProcessSelected: (process: Process) => void; + onShowAlertDetails: (alertId: string) => void; } /** @@ -26,46 +29,69 @@ interface SessionViewDetailPanelDeps { export const SessionViewDetailPanel = ({ alerts, selectedProcess, + investigatedAlert, onProcessSelected, + onShowAlertDetails, }: SessionViewDetailPanelDeps) => { const [selectedTabId, setSelectedTabId] = useState('process'); const processDetail = useMemo(() => getDetailPanelProcess(selectedProcess), [selectedProcess]); const getAlertCount = useCallback(() => { if (!alerts) { - return; + return 0; } return alerts.length >= ALERT_COUNT_THRESHOLD ? ALERT_COUNT_THRESHOLD + '+' : alerts.length; }, [alerts]); - const tabs: EuiTabProps[] = useMemo( - () => [ + const tabs: EuiTabProps[] = useMemo(() => { + const hasAlerts = !!alerts?.length; + + return [ { id: 'process', - name: 'Process', + name: i18n.translate('xpack.sessionView.detailsPanel.process', { + defaultMessage: 'Process', + }), content: , }, { id: 'host', - name: 'Host', + name: i18n.translate('xpack.sessionView.detailsPanel.host', { + defaultMessage: 'Host', + }), content: , }, { id: 'alerts', - name: 'Alerts', + name: i18n.translate('xpack.sessionView.detailsPanel.alerts', { + defaultMessage: 'Alerts', + }), append: ( {getAlertCount()} ), - content: alerts && ( - + disabled: !hasAlerts, + content: hasAlerts && ( + ), }, - ], - [alerts, getAlertCount, processDetail, selectedProcess.events, onProcessSelected] - ); + ]; + }, [ + alerts, + getAlertCount, + processDetail, + selectedProcess.events, + onProcessSelected, + onShowAlertDetails, + investigatedAlert, + ]); const onSelectedTabChanged = useCallback((id: string) => { setSelectedTabId(id); diff --git a/x-pack/plugins/session_view/server/routes/alerts_route.test.ts b/x-pack/plugins/session_view/server/routes/alerts_route.test.ts index 444f7a8f94675..2689b2cd71f62 100644 --- a/x-pack/plugins/session_view/server/routes/alerts_route.test.ts +++ b/x-pack/plugins/session_view/server/routes/alerts_route.test.ts @@ -35,7 +35,7 @@ describe('alerts_route.ts', () => { const body = await doSearch(client, 'asdf'); - expect(body.alerts.length).toBe(0); + expect(body.events.length).toBe(0); }); it('returns results for a particular session entity_id', async () => { @@ -43,7 +43,7 @@ describe('alerts_route.ts', () => { const body = await doSearch(client, 'mockId'); - expect(body.alerts.length).toBe(mockEvents.length); + expect(body.events.length).toBe(mockEvents.length); }); }); }); From e0bf515a9fe0a93fbfca4a7e2978b411af1699be Mon Sep 17 00:00:00 2001 From: mitodrummer Date: Thu, 10 Mar 2022 09:29:40 -0800 Subject: [PATCH 04/13] clean up --- .../public/components/detail_panel_alert_tab/index.tsx | 4 ---- .../public/components/process_tree/hooks.ts | 10 ++++++---- .../components/session_view_detail_panel/index.tsx | 6 +++--- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx index 298ba215f2360..4807547fb8b15 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx @@ -24,10 +24,6 @@ interface DetailPanelAlertTabDeps { const VIEW_MODE_LIST = 'listView'; const VIEW_MODE_GROUP = 'groupView'; -export type AlertsGroup = { - [key: string]: ProcessEvent[]; -}; - /** * Host Panel of session view detail panel. */ diff --git a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts index 5ca403f7f5e8a..991008da14f2d 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts @@ -216,6 +216,7 @@ export const useProcessTree = ({ const [processMap, setProcessMap] = useState(initializedProcessMap); const [processedPages, setProcessedPages] = useState([]); + const [alertsProcessed, setAlertsProcessed] = useState(false); const [searchResults, setSearchResults] = useState([]); const [orphans, setOrphans] = useState([]); @@ -255,10 +256,11 @@ export const useProcessTree = ({ useEffect(() => { // currently we are loading a single page of alerts, with no pagination // so we only need to add these alert events to processMap once. - updateProcessMap(processMap, alerts); - // omitting processMap to avoid a infinite loop - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [alerts]); + if (!alertsProcessed) { + updateProcessMap(processMap, alerts); + setAlertsProcessed(true); + } + }, [processMap, alerts, alertsProcessed]); useEffect(() => { setSearchResults(searchProcessTree(processMap, searchQuery)); diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx index 5e0ddbce8fcfc..1f796ff5edde8 100644 --- a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx @@ -36,7 +36,7 @@ export const SessionViewDetailPanel = ({ const [selectedTabId, setSelectedTabId] = useState('process'); const processDetail = useMemo(() => getDetailPanelProcess(selectedProcess), [selectedProcess]); - const getAlertCount = useCallback(() => { + const alertsCount = useMemo(() => { if (!alerts) { return 0; } @@ -69,7 +69,7 @@ export const SessionViewDetailPanel = ({ }), append: ( - {getAlertCount()} + {alertsCount} ), disabled: !hasAlerts, @@ -85,7 +85,7 @@ export const SessionViewDetailPanel = ({ ]; }, [ alerts, - getAlertCount, + alertsCount, processDetail, selectedProcess.events, onProcessSelected, From 6e51c631cd51f3a7bd59bfbafbc44ee6439784d9 Mon Sep 17 00:00:00 2001 From: mitodrummer Date: Thu, 10 Mar 2022 15:20:32 -0800 Subject: [PATCH 05/13] addressed @opauloh comments --- x-pack/plugins/session_view/common/constants.ts | 4 ++++ .../components/detail_panel_alert_actions/index.tsx | 9 ++++++--- .../components/detail_panel_alert_group_item/index.tsx | 2 +- .../components/detail_panel_alert_list_item/index.tsx | 2 +- .../components/detail_panel_alert_list_item/styles.ts | 7 +------ .../public/components/detail_panel_alert_tab/index.tsx | 4 +++- .../session_view/public/components/session_view/hooks.ts | 6 ++++-- .../public/components/session_view/index.tsx | 2 +- 8 files changed, 21 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/session_view/common/constants.ts b/x-pack/plugins/session_view/common/constants.ts index c0250cb48e70c..f5ecafe3ed917 100644 --- a/x-pack/plugins/session_view/common/constants.ts +++ b/x-pack/plugins/session_view/common/constants.ts @@ -38,3 +38,7 @@ export const ALERTS_PER_PAGE = 1000; // when showing the count of alerts in details panel tab, if the number // exceeds ALERT_COUNT_THRESHOLD we put a + next to it, e.g 999+ export const ALERT_COUNT_THRESHOLD = 999; + +// react-query caching keys +export const QUERY_KEY_PROCESS_EVENTS = 'sessionViewProcessEvents'; +export const QUERY_KEY_ALERTS = 'sessionViewAlerts'; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx index 006a5dccf6f83..b399ac9e7fc29 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx @@ -5,6 +5,7 @@ * 2.0. */ import React, { useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiPopover, EuiContextMenuPanel, EuiButtonIcon, EuiContextMenuItem } from '@elastic/eui'; import { Process, ProcessEvent } from '../../../common/types/process_tree'; @@ -35,13 +36,13 @@ export const DetailPanelAlertActions = ({ setPopover(!isPopoverOpen); }, [isPopoverOpen]); - const onJumpToAlert = () => { + const onJumpToAlert = useCallback(() => { const process = new ProcessImpl(event.process.entity_id); process.addEvent(event); onProcessSelected(process); setPopover(false); - }; + }, [event, onProcessSelected]); const onShowDetails = useCallback(() => { if (event.kibana) { @@ -83,7 +84,9 @@ export const DetailPanelAlertActions = ({ display="empty" size="s" iconType="boxesHorizontal" - aria-label="More" + aria-label={i18n.translate('xpack.sessionView.detailPanelAlertListItem.moreButton', { + defaultMessage: 'More', + })} data-test-subj={BUTTON_TEST_ID} onClick={onToggleMenu} /> diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx index 731e0345c916f..42233c6780df6 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx @@ -30,7 +30,7 @@ export const DetailPanelAlertGroupItem = ({ onProcessSelected, onShowAlertDetails, }: DetailPanelAlertsGroupItemDeps) => { - const styles = useStyles({}); + const styles = useStyles(); const alertsCount = useMemo(() => { return alerts.length >= ALERT_COUNT_THRESHOLD ? ALERT_COUNT_THRESHOLD + '+' : alerts.length; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx index 48583f6787e34..cb4d64393f5f6 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx @@ -43,7 +43,7 @@ export const DetailPanelAlertListItem = ({ isInvestigated, minimal, }: DetailPanelAlertsListItemDeps) => { - const styles = useStyles({ minimal, isInvestigated }); + const styles = useStyles(minimal, isInvestigated); if (!event.kibana) { return null; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts index d0bc1719201bd..7672bb942ff32 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts @@ -9,12 +9,7 @@ import { useMemo } from 'react'; import { useEuiTheme, transparentize } from '@elastic/eui'; import { CSSObject, css } from '@emotion/react'; -interface StylesDeps { - minimal?: boolean; - isInvestigated?: boolean; -} - -export const useStyles = ({ minimal = false, isInvestigated = false }: StylesDeps) => { +export const useStyles = (minimal = false, isInvestigated = false) => { const { euiTheme } = useEuiTheme(); const cached = useMemo(() => { diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx index 4807547fb8b15..074a117875632 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx @@ -67,7 +67,9 @@ export const DetailPanelAlertTab = ({ { let { cursor } = pageParam; const { forward } = pageParam; @@ -81,7 +83,7 @@ export const useFetchSessionViewProcessEvents = ( export const useFetchSessionViewAlerts = (sessionEntityId: string) => { const { http } = useKibana().services; const query = useQuery( - 'sessionViewAlerts', + QUERY_KEY_ALERTS, async () => { const res = await http.get(ALERTS_ROUTE, { query: { diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx index eeeb5d0e2e1ef..24fdcc7d5fd84 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -65,7 +65,7 @@ export const SessionView = ({ sessionEntityId, height, jumpToEvent }: SessionVie const alertsQuery = useFetchSessionViewAlerts(sessionEntityId); const { data: alerts, error: alertsError, isFetching: alertsFetching } = alertsQuery; - const hasData = data && alerts && data.pages.length > 0 && data.pages[0].events.length > 0; + const hasData = alerts && data && data.pages?.[0].events.length > 0; const hasError = error || alertsError; const renderIsLoading = (isFetching || alertsFetching) && !data; const renderDetails = isDetailOpen && selectedProcess; From 23e3cb2f3f2ef321040eac053d9f041611732ffd Mon Sep 17 00:00:00 2001 From: mitodrummer Date: Mon, 14 Mar 2022 13:27:21 -0700 Subject: [PATCH 06/13] fixed weird bug due to importing assests from a test into its component --- .../plugins/session_view/common/constants.ts | 2 +- .../detail_panel_alert_actions/index.test.tsx | 11 ++++---- .../detail_panel_alert_actions/index.tsx | 5 +++- .../detail_panel_alert_group_item/index.tsx | 13 ++++----- .../detail_panel_alert_list_item/index.tsx | 13 ++++----- .../detail_panel_alert_tab/index.test.tsx | 28 ++++++++++--------- .../detail_panel_alert_tab/index.tsx | 4 ++- 7 files changed, 41 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/session_view/common/constants.ts b/x-pack/plugins/session_view/common/constants.ts index 82b303141b9c6..abc693f5a5013 100644 --- a/x-pack/plugins/session_view/common/constants.ts +++ b/x-pack/plugins/session_view/common/constants.ts @@ -43,4 +43,4 @@ export const ALERT_COUNT_THRESHOLD = 999; export const QUERY_KEY_PROCESS_EVENTS = 'sessionViewProcessEvents'; export const QUERY_KEY_ALERTS = 'sessionViewAlerts'; -export const MOUSE_EVENT_PLACEHOLDER = { stopPropagation: () => undefined } as React.MouseEvent; \ No newline at end of file +export const MOUSE_EVENT_PLACEHOLDER = { stopPropagation: () => undefined } as React.MouseEvent; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx index 4ef537d89cfd4..1d0c9d0227699 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx @@ -7,15 +7,16 @@ import React from 'react'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; -import { DetailPanelAlertActions } from './index'; +import { + DetailPanelAlertActions, + BUTTON_TEST_ID, + SHOW_DETAILS_TEST_ID, + JUMP_TO_PROCESS_TEST_ID, +} from './index'; import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock'; import userEvent from '@testing-library/user-event'; import { ProcessImpl } from '../process_tree/hooks'; -export const BUTTON_TEST_ID = 'sessionView:detailPanelAlertActionsBtn'; -export const SHOW_DETAILS_TEST_ID = 'sessionView:detailPanelAlertActionShowDetails'; -export const JUMP_TO_PROCESS_TEST_ID = 'sessionView:detailPanelAlertActionJumpToProcess'; - describe('DetailPanelAlertActions component', () => { let render: () => ReturnType; let renderResult: ReturnType; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx index b399ac9e7fc29..4c7e3fdfaa961 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx @@ -10,7 +10,10 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiPopover, EuiContextMenuPanel, EuiButtonIcon, EuiContextMenuItem } from '@elastic/eui'; import { Process, ProcessEvent } from '../../../common/types/process_tree'; import { ProcessImpl } from '../process_tree/hooks'; -import { BUTTON_TEST_ID, SHOW_DETAILS_TEST_ID, JUMP_TO_PROCESS_TEST_ID } from './index.test'; + +export const BUTTON_TEST_ID = 'sessionView:detailPanelAlertActionsBtn'; +export const SHOW_DETAILS_TEST_ID = 'sessionView:detailPanelAlertActionShowDetails'; +export const JUMP_TO_PROCESS_TEST_ID = 'sessionView:detailPanelAlertActionJumpToProcess'; interface DetailPanelAlertActionsDeps { event: ProcessEvent; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx index 42233c6780df6..daa472cd6e5b4 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx @@ -10,11 +10,10 @@ import { Process, ProcessEvent } from '../../../common/types/process_tree'; import { useStyles } from '../detail_panel_alert_list_item/styles'; import { DetailPanelAlertListItem } from '../detail_panel_alert_list_item'; import { ALERT_COUNT_THRESHOLD } from '../../../common/constants'; -import { - ALERT_GROUP_ITEM_TEST_ID, - ALERT_GROUP_ITEM_TITLE, - ALERT_GROUP_ITEM_COUNT, -} from '../detail_panel_alert_tab/index.test'; + +export const ALERT_GROUP_ITEM_TEST_ID = 'sessionView:detailPanelAlertGroupItem'; +export const ALERT_GROUP_ITEM_COUNT_TEST_ID = 'sessionView:detailPanelAlertGroupCount'; +export const ALERT_GROUP_ITEM_TITLE_TEST_ID = 'sessionView:detailPanelAlertGroupTitle'; interface DetailPanelAlertsGroupItemDeps { alerts: ProcessEvent[]; @@ -49,7 +48,7 @@ export const DetailPanelAlertGroupItem = ({ arrowDisplay="right" initialIsOpen={false} buttonContent={ - +

{rule.name} @@ -59,7 +58,7 @@ export const DetailPanelAlertGroupItem = ({ css={styles.alertItem} extraAction={ diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx index cb4d64393f5f6..516d04539432e 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx @@ -19,11 +19,10 @@ import { import { Process, ProcessEvent } from '../../../common/types/process_tree'; import { useStyles } from './styles'; import { DetailPanelAlertActions } from '../detail_panel_alert_actions'; -import { - ALERT_LIST_ITEM_TEST_ID, - ALERT_LIST_ITEM_TIMESTAMP, - ALERT_LIST_ITEM_ARGS, -} from '../detail_panel_alert_tab/index.test'; + +export const ALERT_LIST_ITEM_TEST_ID = 'sessionView:detailPanelAlertListItem'; +export const ALERT_LIST_ITEM_ARGS_TEST_ID = 'sessionView:detailPanelAlertListItemArgs'; +export const ALERT_LIST_ITEM_TIMESTAMP_TEST_ID = 'sessionView:detailPanelAlertListItemTimestamp'; interface DetailPanelAlertsListItemDeps { event: ProcessEvent; @@ -109,7 +108,7 @@ export const DetailPanelAlertListItem = ({ } > - + {timestamp} - + {args.join(' ')} diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx index 737a04b802090..c77195796cc36 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx @@ -11,15 +11,17 @@ import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; import { DetailPanelAlertTab } from './index'; import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock'; import { fireEvent } from '@testing-library/dom'; - -export const ALERT_LIST_ITEM_TEST_ID = 'sessionView:detailPanelAlertListItem'; -export const ALERT_GROUP_ITEM_TEST_ID = 'sessionView:detailPanelAlertGroupItem'; -export const INVESTIGATED_ALERT_TEST_ID = 'sessionView:detailPanelInvestigatedAlert'; -export const VIEW_MODE_TOGGLE = 'sessionView:detailPanelAlertsViewMode'; -export const ALERT_LIST_ITEM_TIMESTAMP = 'sessionView:detailPanelAlertListItemTimestamp'; -export const ALERT_LIST_ITEM_ARGS = 'sessionView:detailPanelAlertListItemArgs'; -export const ALERT_GROUP_ITEM_COUNT = 'sessionView:detailPanelAlertGroupCount'; -export const ALERT_GROUP_ITEM_TITLE = 'sessionView:detailPanelAlertGroupTitle'; +import { INVESTIGATED_ALERT_TEST_ID, VIEW_MODE_TOGGLE } from './index'; +import { + ALERT_LIST_ITEM_TEST_ID, + ALERT_LIST_ITEM_ARGS_TEST_ID, + ALERT_LIST_ITEM_TIMESTAMP_TEST_ID, +} from '../detail_panel_alert_list_item/index'; +import { + ALERT_GROUP_ITEM_TEST_ID, + ALERT_GROUP_ITEM_COUNT_TEST_ID, + ALERT_GROUP_ITEM_TITLE_TEST_ID, +} from '../detail_panel_alert_group_item/index'; const ACCORDION_BUTTON_CLASS = '.euiAccordion__button'; const VIEW_MODE_GROUP = 'groupView'; @@ -204,11 +206,11 @@ describe('DetailPanelAlertTab component', () => { /> ); - expect(renderResult.queryAllByTestId(ALERT_LIST_ITEM_TIMESTAMP)[0]).toHaveTextContent( + expect(renderResult.queryAllByTestId(ALERT_LIST_ITEM_TIMESTAMP_TEST_ID)[0]).toHaveTextContent( mockAlerts[0]['@timestamp'] ); - expect(renderResult.queryAllByTestId(ALERT_LIST_ITEM_ARGS)[0]).toHaveTextContent( + expect(renderResult.queryAllByTestId(ALERT_LIST_ITEM_ARGS_TEST_ID)[0]).toHaveTextContent( mockAlerts[0].process.args.join(' ') ); }); @@ -224,8 +226,8 @@ describe('DetailPanelAlertTab component', () => { fireEvent.click(renderResult.getByTestId(VIEW_MODE_GROUP)); - expect(renderResult.queryByTestId(ALERT_GROUP_ITEM_COUNT)).toHaveTextContent('2'); - expect(renderResult.queryByTestId(ALERT_GROUP_ITEM_TITLE)).toHaveTextContent( + expect(renderResult.queryByTestId(ALERT_GROUP_ITEM_COUNT_TEST_ID)).toHaveTextContent('2'); + expect(renderResult.queryByTestId(ALERT_GROUP_ITEM_TITLE_TEST_ID)).toHaveTextContent( mockAlerts[0].kibana?.alert.rule.name || '' ); }); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx index 074a117875632..fdd5b605eaca6 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx @@ -12,7 +12,9 @@ import { ProcessEvent, Process } from '../../../common/types/process_tree'; import { useStyles } from './styles'; import { DetailPanelAlertListItem } from '../detail_panel_alert_list_item'; import { DetailPanelAlertGroupItem } from '../detail_panel_alert_group_item'; -import { INVESTIGATED_ALERT_TEST_ID, VIEW_MODE_TOGGLE } from './index.test'; + +export const INVESTIGATED_ALERT_TEST_ID = 'sessionView:detailPanelInvestigatedAlert'; +export const VIEW_MODE_TOGGLE = 'sessionView:detailPanelAlertsViewMode'; interface DetailPanelAlertTabDeps { alerts: ProcessEvent[]; From 1411a54d5d71e160b66404ffb7aa96d982c0fa4b Mon Sep 17 00:00:00 2001 From: mitodrummer Date: Mon, 14 Mar 2022 16:35:35 -0700 Subject: [PATCH 07/13] empty state added for alerts tab --- .../detail_panel_alert_tab/index.test.tsx | 18 +++++++++++- .../detail_panel_alert_tab/index.tsx | 28 ++++++++++++++++++- .../session_view_detail_panel/index.tsx | 5 ++-- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx index c77195796cc36..a915f8e285ad1 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx @@ -11,7 +11,11 @@ import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; import { DetailPanelAlertTab } from './index'; import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock'; import { fireEvent } from '@testing-library/dom'; -import { INVESTIGATED_ALERT_TEST_ID, VIEW_MODE_TOGGLE } from './index'; +import { + INVESTIGATED_ALERT_TEST_ID, + VIEW_MODE_TOGGLE, + ALERTS_TAB_EMPTY_STATE_TEST_ID, +} from './index'; import { ALERT_LIST_ITEM_TEST_ID, ALERT_LIST_ITEM_ARGS_TEST_ID, @@ -231,5 +235,17 @@ describe('DetailPanelAlertTab component', () => { mockAlerts[0].kibana?.alert.rule.name || '' ); }); + + it('renders an empty state when there are no alerts', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryByTestId(ALERTS_TAB_EMPTY_STATE_TEST_ID)).toBeTruthy(); + }); }); }); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx index fdd5b605eaca6..7fa47f4f5daf7 100644 --- a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx @@ -5,14 +5,16 @@ * 2.0. */ import React, { useState, useMemo } from 'react'; -import { EuiButtonGroup, EuiHorizontalRule } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiButtonGroup, EuiHorizontalRule } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; import { groupBy } from 'lodash'; import { ProcessEvent, Process } from '../../../common/types/process_tree'; import { useStyles } from './styles'; import { DetailPanelAlertListItem } from '../detail_panel_alert_list_item'; import { DetailPanelAlertGroupItem } from '../detail_panel_alert_group_item'; +export const ALERTS_TAB_EMPTY_STATE_TEST_ID = 'sessionView:detailPanelAlertsEmptyState'; export const INVESTIGATED_ALERT_TEST_ID = 'sessionView:detailPanelInvestigatedAlert'; export const VIEW_MODE_TOGGLE = 'sessionView:detailPanelAlertsViewMode'; @@ -64,6 +66,30 @@ export const DetailPanelAlertTab = ({ return groupBy(filteredAlerts, (event) => event.kibana?.alert.rule.uuid); }, [filteredAlerts]); + if (alerts.length === 0) { + return ( + + + + } + body={ +

+ +

+ } + /> + ); + } + return (
{alertsCount} ), - disabled: !hasAlerts, - content: hasAlerts && ( + content: alerts && ( Date: Wed, 16 Mar 2022 10:07:28 -0700 Subject: [PATCH 08/13] react-query caching keys updated to include sessionEntityId --- .../session_view/public/components/session_view/hooks.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/session_view/public/components/session_view/hooks.ts b/x-pack/plugins/session_view/public/components/session_view/hooks.ts index 687de26dcfc9c..c9d66493edfbf 100644 --- a/x-pack/plugins/session_view/public/components/session_view/hooks.ts +++ b/x-pack/plugins/session_view/public/components/session_view/hooks.ts @@ -25,9 +25,10 @@ export const useFetchSessionViewProcessEvents = ( const { http } = useKibana().services; const [isJumpToFirstPage, setIsJumpToFirstPage] = useState(false); const jumpToCursor = jumpToEvent && jumpToEvent.process.start; + const cachingKeys = [QUERY_KEY_PROCESS_EVENTS, sessionEntityId]; const query = useInfiniteQuery( - QUERY_KEY_PROCESS_EVENTS, + cachingKeys, async ({ pageParam = {} }) => { let { cursor } = pageParam; const { forward } = pageParam; @@ -83,8 +84,9 @@ export const useFetchSessionViewProcessEvents = ( export const useFetchSessionViewAlerts = (sessionEntityId: string) => { const { http } = useKibana().services; + const cachingKeys = [QUERY_KEY_ALERTS, sessionEntityId]; const query = useQuery( - QUERY_KEY_ALERTS, + cachingKeys, async () => { const res = await http.get(ALERTS_ROUTE, { query: { From 79a929e1bfee263aebb7fecc8f1120ae41e2093a Mon Sep 17 00:00:00 2001 From: mitodrummer Date: Mon, 21 Mar 2022 16:30:46 -0700 Subject: [PATCH 09/13] rule_registry added as a dependency in order to use AlertsClient in alerts_route.ts --- .../common/assets/field_maps/ecs_field_map.ts | 15 +++ .../plugins/session_view/common/constants.ts | 1 - x-pack/plugins/session_view/kibana.json | 7 +- .../components/process_tree_node/index.tsx | 36 ++++++- x-pack/plugins/session_view/server/plugin.ts | 12 ++- .../server/routes/alerts_route.test.ts | 100 ++++++++++++++++-- .../server/routes/alerts_route.ts | 47 ++++---- .../session_view/server/routes/index.ts | 5 +- x-pack/plugins/session_view/server/types.ts | 15 ++- x-pack/plugins/session_view/tsconfig.json | 3 +- 10 files changed, 189 insertions(+), 52 deletions(-) diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts index dc81e200032f7..c3e3aa5c604db 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts @@ -2401,6 +2401,21 @@ export const ecsFieldMap = { array: false, required: false, }, + 'process.entry_leader.entity_id': { + type: 'keyword', + array: false, + required: false, + }, + 'process.session_leader.entity_id': { + type: 'keyword', + array: false, + required: false, + }, + 'process.group_leader.entity_id': { + type: 'keyword', + array: false, + required: false, + }, 'process.executable': { type: 'keyword', array: false, diff --git a/x-pack/plugins/session_view/common/constants.ts b/x-pack/plugins/session_view/common/constants.ts index abc693f5a5013..bcf21961a50f4 100644 --- a/x-pack/plugins/session_view/common/constants.ts +++ b/x-pack/plugins/session_view/common/constants.ts @@ -9,7 +9,6 @@ export const PROCESS_EVENTS_ROUTE = '/internal/session_view/process_events_route export const ALERTS_ROUTE = '/internal/session_view/alerts_route'; export const SESSION_ENTRY_LEADERS_ROUTE = '/internal/session_view/session_entry_leaders_route'; export const PROCESS_EVENTS_INDEX = 'logs-endpoint.events.process-default'; -export const ALERTS_INDEX = '.siem-signals-default'; export const ENTRY_SESSION_ENTITY_ID_PROPERTY = 'process.entry_leader.entity_id'; // We fetch a large number of events per page to mitigate a few design caveats in session viewer diff --git a/x-pack/plugins/session_view/kibana.json b/x-pack/plugins/session_view/kibana.json index ff9d849016c55..4807315569d34 100644 --- a/x-pack/plugins/session_view/kibana.json +++ b/x-pack/plugins/session_view/kibana.json @@ -1,6 +1,6 @@ { "id": "sessionView", - "version": "8.0.0", + "version": "1.0.0", "kibanaVersion": "kibana", "owner": { "name": "Security Team", @@ -8,10 +8,11 @@ }, "requiredPlugins": [ "data", - "timelines" + "timelines", + "ruleRegistry" ], "requiredBundles": [ - "kibanaReact", + "kibanaReact", "esUiShared" ], "server": true, diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx index f73cc706fd398..9894e0deab015 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx @@ -20,7 +20,8 @@ import React, { useCallback, useMemo, } from 'react'; -import { EuiButton, EuiIcon } from '@elastic/eui'; +import { EuiButton, EuiIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { Process } from '../../../common/types/process_tree'; import { useStyles } from './styles'; @@ -120,6 +121,33 @@ export function ProcessTreeNode({ }; const processDetails = process.getDetails(); + const hasExec = process.hasExec(); + + const processIcon = useMemo(() => { + if (!process.parent) { + return 'unlink'; + } else if (hasExec) { + return 'console'; + } else { + return 'branch'; + } + }, [hasExec, process.parent]); + + const iconTooltip = useMemo(() => { + if (!process.parent) { + return i18n.translate('xpack.sessionView.processNode.tooltipOrphan', { + defaultMessage: 'Process missing parent (orphan)', + }); + } else if (hasExec) { + return i18n.translate('xpack.sessionView.processNode.tooltipExec', { + defaultMessage: "Process exec'd", + }); + } else { + return i18n.translate('xpack.sessionView.processNode.tooltipFork', { + defaultMessage: 'Process forked (no exec)', + }); + } + }, [hasExec, process.parent]); if (!processDetails?.process) { return null; @@ -144,11 +172,9 @@ export function ProcessTreeNode({ const showUserEscalation = user.id !== parent.user.id; const interactiveSession = !!tty; const sessionIcon = interactiveSession ? 'consoleApp' : 'compute'; - const hasExec = process.hasExec(); const iconTestSubj = hasExec ? 'sessionView:processTreeNodeExecIcon' : 'sessionView:processTreeNodeForkIcon'; - const processIcon = hasExec ? 'console' : 'branch'; return (
@@ -178,7 +204,9 @@ export function ProcessTreeNode({ ) : ( - + + + {' '} {workingDirectory}  {args[0]}  diff --git a/x-pack/plugins/session_view/server/plugin.ts b/x-pack/plugins/session_view/server/plugin.ts index c7fd511b3de05..7347f7676af62 100644 --- a/x-pack/plugins/session_view/server/plugin.ts +++ b/x-pack/plugins/session_view/server/plugin.ts @@ -11,12 +11,14 @@ import { Plugin, Logger, PluginInitializerContext, + IRouter, } from '../../../../src/core/server'; import { SessionViewSetupPlugins, SessionViewStartPlugins } from './types'; import { registerRoutes } from './routes'; export class SessionViewPlugin implements Plugin { private logger: Logger; + private router: IRouter | undefined; /** * Initialize SessionViewPlugin class properties (logger, etc) that is accessible @@ -28,14 +30,16 @@ export class SessionViewPlugin implements Plugin { public setup(core: CoreSetup, plugins: SessionViewSetupPlugins) { this.logger.debug('session view: Setup'); - const router = core.http.createRouter(); - - // Register server routes - registerRoutes(router); + this.router = core.http.createRouter(); } public start(core: CoreStart, plugins: SessionViewStartPlugins) { this.logger.debug('session view: Start'); + + // Register server routes + if (this.router) { + registerRoutes(this.router, plugins.ruleRegistry); + } } public stop() { diff --git a/x-pack/plugins/session_view/server/routes/alerts_route.test.ts b/x-pack/plugins/session_view/server/routes/alerts_route.test.ts index 2689b2cd71f62..4c8ee6fb2c83e 100644 --- a/x-pack/plugins/session_view/server/routes/alerts_route.test.ts +++ b/x-pack/plugins/session_view/server/routes/alerts_route.test.ts @@ -4,14 +4,35 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { + ALERT_RULE_CONSUMER, + ALERT_RULE_TYPE_ID, + SPACE_IDS, + ALERT_WORKFLOW_STATUS, +} from '@kbn/rule-data-utils'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { doSearch } from './alerts_route'; import { mockEvents } from '../../common/mocks/constants/session_view_process.mock'; +import { + AlertsClient, + ConstructorOptions, +} from '../../../rule_registry/server/alert_data_client/alerts_client'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { alertingAuthorizationMock } from '../../../alerting/server/authorization/alerting_authorization.mock'; +import { auditLoggerMock } from '../../../security/server/audit/mocks'; +import { AlertingAuthorizationEntity } from '../../../alerting/server'; +import { ruleDataServiceMock } from '../../../rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock'; + +const alertingAuthMock = alertingAuthorizationMock.create(); +const auditLogger = auditLoggerMock.create(); + +const DEFAULT_SPACE = 'test_default_space_id'; + const getEmptyResponse = async () => { return { hits: { - total: { value: 0, relation: 'eq' }, + total: 0, hits: [], }, }; @@ -20,28 +41,91 @@ const getEmptyResponse = async () => { const getResponse = async () => { return { hits: { - total: { value: mockEvents.length, relation: 'eq' }, + total: mockEvents.length, hits: mockEvents.map((event) => { - return { _source: event }; + return { + found: true, + _type: 'alert', + _index: '.alerts-security', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _source: { + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + message: 'hello world 1', + [ALERT_RULE_CONSUMER]: 'apm', + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['test_default_space_id'], + ...event, + }, + }; }), }, }; }; +const esClientMock = elasticsearchServiceMock.createElasticsearchClient(getResponse()); + +const alertsClientParams: jest.Mocked = { + logger: loggingSystemMock.create().get(), + authorization: alertingAuthMock, + auditLogger, + ruleDataService: ruleDataServiceMock.create(), + esClient: esClientMock, +}; + describe('alerts_route.ts', () => { + beforeEach(() => { + jest.resetAllMocks(); + + alertingAuthMock.getSpaceId.mockImplementation(() => DEFAULT_SPACE); + // @ts-expect-error + alertingAuthMock.getAuthorizationFilter.mockImplementation(async () => + Promise.resolve({ filter: [] }) + ); + // @ts-expect-error + alertingAuthMock.getAugmentedRuleTypesWithAuthorization.mockImplementation(async () => { + const authorizedRuleTypes = new Set(); + authorizedRuleTypes.add({ producer: 'apm' }); + return Promise.resolve({ authorizedRuleTypes }); + }); + + alertingAuthMock.ensureAuthorized.mockImplementation( + // @ts-expect-error + async ({ + ruleTypeId, + consumer, + operation, + entity, + }: { + ruleTypeId: string; + consumer: string; + operation: string; + entity: typeof AlertingAuthorizationEntity.Alert; + }) => { + if (ruleTypeId === 'apm.error_rate' && consumer === 'apm') { + return Promise.resolve(); + } + return Promise.reject(new Error(`Unauthorized for ${ruleTypeId} and ${consumer}`)); + } + ); + }); + describe('doSearch(client, sessionEntityId)', () => { it('should return an empty events array for a non existant entity_id', async () => { - const client = elasticsearchServiceMock.createElasticsearchClient(getEmptyResponse()); - - const body = await doSearch(client, 'asdf'); + const esClient = elasticsearchServiceMock.createElasticsearchClient(getEmptyResponse()); + const alertsClient = new AlertsClient({ ...alertsClientParams, esClient }); + const body = await doSearch(alertsClient, 'asdf'); expect(body.events.length).toBe(0); }); it('returns results for a particular session entity_id', async () => { - const client = elasticsearchServiceMock.createElasticsearchClient(getResponse()); + const esClient = elasticsearchServiceMock.createElasticsearchClient(getResponse()); + const alertsClient = new AlertsClient({ ...alertsClientParams, esClient }); - const body = await doSearch(client, 'mockId'); + const body = await doSearch(alertsClient, 'asdf'); expect(body.events.length).toBe(mockEvents.length); }); diff --git a/x-pack/plugins/session_view/server/routes/alerts_route.ts b/x-pack/plugins/session_view/server/routes/alerts_route.ts index 7fed65b49e1bf..3d03cb5cb8214 100644 --- a/x-pack/plugins/session_view/server/routes/alerts_route.ts +++ b/x-pack/plugins/session_view/server/routes/alerts_route.ts @@ -5,17 +5,19 @@ * 2.0. */ import { schema } from '@kbn/config-schema'; -import type { ElasticsearchClient } from 'kibana/server'; import { IRouter } from '../../../../../src/core/server'; import { ALERTS_ROUTE, ALERTS_PER_PAGE, - ALERTS_INDEX, ENTRY_SESSION_ENTITY_ID_PROPERTY, } from '../../common/constants'; import { expandDottedObject } from '../../common/utils/expand_dotted_object'; +import type { AlertsClient, RuleRegistryPluginStartContract } from '../../../rule_registry/server'; -export const registerAlertsRoute = (router: IRouter) => { +export const registerAlertsRoute = ( + router: IRouter, + ruleRegistry: RuleRegistryPluginStartContract +) => { router.get( { path: ALERTS_ROUTE, @@ -26,7 +28,7 @@ export const registerAlertsRoute = (router: IRouter) => { }, }, async (context, request, response) => { - const client = context.core.elasticsearch.client.asCurrentUser; + const client = await ruleRegistry.getRacClientWithRequest(request); const { sessionEntityId } = request.query; const body = await doSearch(client, sessionEntityId); @@ -35,31 +37,26 @@ export const registerAlertsRoute = (router: IRouter) => { ); }; -export const doSearch = async (client: ElasticsearchClient, sessionEntityId: string) => { - const search = await client.search({ - index: [ALERTS_INDEX], - ignore_unavailable: true, // on a new installation the .siem-signals-default index might not be created yet. - body: { - query: { - match: { - [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId, - }, - }, - // This runtime_mappings is a temporary fix, so we are able to Query these ECS fields while they are not available - // TODO: Remove the runtime_mappings once process.entry_leader.entity_id is implemented to ECS - runtime_mappings: { - [ENTRY_SESSION_ENTITY_ID_PROPERTY]: { - type: 'keyword', - }, +export const doSearch = async (client: AlertsClient, sessionEntityId: string) => { + const indices = await client.getAuthorizedAlertsIndices(['siem']); + + if (!indices) { + return { events: [] }; + } + + const results = await client.find({ + query: { + match: { + [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId, }, - size: ALERTS_PER_PAGE, - sort: [{ '@timestamp': 'asc' }], }, + track_total_hits: false, + size: ALERTS_PER_PAGE, + index: indices.join(','), }); - const events = search.hits.hits.map((hit: any) => { - // TODO: re-eval if this is needed after updated ECS mappings are applied. - // the .siem-signals-default index flattens many properties. this util unflattens them. + const events = results.hits.hits.map((hit: any) => { + // the alert indexes flattens many properties. this util unflattens them as session view expects structured json. hit._source = expandDottedObject(hit._source); return hit; diff --git a/x-pack/plugins/session_view/server/routes/index.ts b/x-pack/plugins/session_view/server/routes/index.ts index b07a416fc05bd..9d6d028f37118 100644 --- a/x-pack/plugins/session_view/server/routes/index.ts +++ b/x-pack/plugins/session_view/server/routes/index.ts @@ -8,9 +8,10 @@ import { IRouter } from '../../../../../src/core/server'; import { registerProcessEventsRoute } from './process_events_route'; import { registerAlertsRoute } from './alerts_route'; import { sessionEntryLeadersRoute } from './session_entry_leaders_route'; +import { RuleRegistryPluginStartContract } from '../../../rule_registry/server'; -export const registerRoutes = (router: IRouter) => { +export const registerRoutes = (router: IRouter, ruleRegistry: RuleRegistryPluginStartContract) => { registerProcessEventsRoute(router); sessionEntryLeadersRoute(router); - registerAlertsRoute(router); + registerAlertsRoute(router, ruleRegistry); }; diff --git a/x-pack/plugins/session_view/server/types.ts b/x-pack/plugins/session_view/server/types.ts index 0d1375081ca87..29995077ccfbe 100644 --- a/x-pack/plugins/session_view/server/types.ts +++ b/x-pack/plugins/session_view/server/types.ts @@ -4,8 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { + RuleRegistryPluginSetupContract as RuleRegistryPluginSetup, + RuleRegistryPluginStartContract as RuleRegistryPluginStart, +} from '../../rule_registry/server'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface SessionViewSetupPlugins {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface SessionViewStartPlugins {} +export interface SessionViewSetupPlugins { + ruleRegistry: RuleRegistryPluginSetup; +} + +export interface SessionViewStartPlugins { + ruleRegistry: RuleRegistryPluginStart; +} diff --git a/x-pack/plugins/session_view/tsconfig.json b/x-pack/plugins/session_view/tsconfig.json index a99e83976a31d..0a21d320dfb29 100644 --- a/x-pack/plugins/session_view/tsconfig.json +++ b/x-pack/plugins/session_view/tsconfig.json @@ -37,6 +37,7 @@ { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, { "path": "../infra/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_utils/tsconfig.json" } + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../rule_registry/tsconfig.json" } ] } From 38288cf2b2cf340ad8008833504dd102d297c8fc Mon Sep 17 00:00:00 2001 From: mitodrummer Date: Mon, 21 Mar 2022 21:56:57 -0700 Subject: [PATCH 10/13] fixed build/test errors due to merge. events route now orders by process.start then @timestamp --- .../public/components/process_tree/index.test.tsx | 5 ++++- .../server/routes/process_events_route.ts | 14 ++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx index 77f32b68f37d4..6afc8afe2d556 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { mockData, mockAlerts } from '../../../common/mocks/constants/session_view_process.mock'; -import { mockData } from '../../../common/mocks/constants/session_view_process.mock'; import { Process } from '../../../common/types/process_tree'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; import { ProcessImpl } from './hooks'; @@ -52,6 +51,7 @@ describe('ProcessTree component', () => { true} hasNextPage={false} @@ -75,6 +75,7 @@ describe('ProcessTree component', () => { true} hasNextPage={false} @@ -94,6 +95,7 @@ describe('ProcessTree component', () => { true} hasNextPage={false} @@ -118,6 +120,7 @@ describe('ProcessTree component', () => { true} hasNextPage={false} diff --git a/x-pack/plugins/session_view/server/routes/process_events_route.ts b/x-pack/plugins/session_view/server/routes/process_events_route.ts index 524bace430adf..7be1885c70ab1 100644 --- a/x-pack/plugins/session_view/server/routes/process_events_route.ts +++ b/x-pack/plugins/session_view/server/routes/process_events_route.ts @@ -50,15 +50,13 @@ export const doSearch = async ( [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId, }, }, - // This runtime_mappings is a temporary fix, so we are able to Query these ECS fields while they are not available - // TODO: Remove the runtime_mappings once process.entry_leader.entity_id is implemented to ECS - runtime_mappings: { - [ENTRY_SESSION_ENTITY_ID_PROPERTY]: { - type: 'keyword', - }, - }, size: PROCESS_EVENTS_PER_PAGE, - sort: [{ '@timestamp': forward ? 'asc' : 'desc' }], + // we first sort by process.start, this allows lifecycle events to load all at once for a given process, and + // avoid issues like where the session leaders 'end' event is loaded at the very end of what could be multiple pages of events + sort: [ + { 'process.start': forward ? 'asc' : 'desc' }, + { '@timestamp': forward ? 'asc' : 'desc' }, + ], search_after: cursor ? [cursor] : undefined, }, }); From af90b971d1a6d90e4189be32c1513323ec7345f0 Mon Sep 17 00:00:00 2001 From: mitodrummer Date: Tue, 22 Mar 2022 15:39:20 -0700 Subject: [PATCH 11/13] plumbing for the alert details tie in done. --- .../public/components/process_tree/hooks.ts | 1 + .../components/process_tree/index.test.tsx | 2 +- .../public/components/process_tree/index.tsx | 9 +++------ .../process_tree_alert/index.test.tsx | 10 +++++----- .../components/process_tree_alert/index.tsx | 12 +++++------- .../process_tree_alerts/index.test.tsx | 2 +- .../components/process_tree_alerts/index.tsx | 9 +++------ .../process_tree_node/index.test.tsx | 2 +- .../components/process_tree_node/index.tsx | 12 ++++-------- .../public/components/session_view/hooks.ts | 5 +++-- .../public/components/session_view/index.tsx | 18 +++++++++++++----- 11 files changed, 40 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts index 866c3adcea9b3..2b7f78e88fafb 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts @@ -199,6 +199,7 @@ export const useProcessTree = ({ sessionEntityId, data, alerts, + searchQuery, updatedAlertsStatus, }: UseProcessTreeDeps) => { // initialize map, as well as a placeholder for session leader process diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx index 2635ba3b6bc4e..3c0b9c5d0d4d9 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx @@ -29,7 +29,7 @@ describe('ProcessTree component', () => { hasPreviousPage: false, onProcessSelected: jest.fn(), updatedAlertsStatus: {}, - handleOnAlertDetailsClosed: jest.fn(), + onShowAlertDetails: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.tsx index 406c64b9f8ea0..1e10e58d1cca0 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree/index.tsx @@ -45,8 +45,7 @@ export interface ProcessTreeDeps { // a map for alerts with updated status and process.entity_id updatedAlertsStatus: AlertStatusEventEntityIdMap; - loadAlertDetails?: (alertUuid: string, handleOnAlertDetailsClosed: () => void) => void; - handleOnAlertDetailsClosed: (alertUuid: string) => void; + onShowAlertDetails: (alertUuid: string) => void; timeStampOn?: boolean; verboseModeOn?: boolean; } @@ -66,8 +65,7 @@ export const ProcessTree = ({ onProcessSelected, setSearchResults, updatedAlertsStatus, - loadAlertDetails, - handleOnAlertDetailsClosed, + onShowAlertDetails, timeStampOn, verboseModeOn, }: ProcessTreeDeps) => { @@ -206,8 +204,7 @@ export const ProcessTree = ({ selectedProcessId={selectedProcess?.id} scrollerRef={scrollerRef} onChangeJumpToEventVisibility={onChangeJumpToEventVisibility} - loadAlertDetails={loadAlertDetails} - handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} + onShowAlertDetails={onShowAlertDetails} timeStampOn={timeStampOn} verboseModeOn={verboseModeOn} /> diff --git a/x-pack/plugins/session_view/public/components/process_tree_alert/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_alert/index.test.tsx index 2a56a0ae2be67..c1b0c807528ec 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alert/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alert/index.test.tsx @@ -26,7 +26,7 @@ describe('ProcessTreeAlerts component', () => { isSelected: false, onClick: jest.fn(), selectAlert: jest.fn(), - handleOnAlertDetailsClosed: jest.fn(), + onShowAlertDetails: jest.fn(), }; beforeEach(() => { @@ -61,16 +61,16 @@ describe('ProcessTreeAlerts component', () => { expect(selectAlert).toHaveBeenCalledTimes(1); }); - it('should execute loadAlertDetails callback when clicking on expand button', async () => { - const loadAlertDetails = jest.fn(); + it('should execute onShowAlertDetails callback when clicking on expand button', async () => { + const onShowAlertDetails = jest.fn(); renderResult = mockedContext.render( - + ); const expandButton = renderResult.queryByTestId(EXPAND_BUTTON_TEST_ID); expect(expandButton).toBeTruthy(); expandButton?.click(); - expect(loadAlertDetails).toHaveBeenCalledTimes(1); + expect(onShowAlertDetails).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx index 5ec1c4a7693c3..30892d02c5428 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx @@ -17,8 +17,7 @@ export interface ProcessTreeAlertDeps { isSelected: boolean; onClick: (alert: ProcessEventAlert | null) => void; selectAlert: (alertUuid: string) => void; - loadAlertDetails?: (alertUuid: string, handleOnAlertDetailsClosed: () => void) => void; - handleOnAlertDetailsClosed: (alertUuid: string, status?: string) => void; + onShowAlertDetails: (alertUuid: string) => void; } export const ProcessTreeAlert = ({ @@ -27,8 +26,7 @@ export const ProcessTreeAlert = ({ isSelected, onClick, selectAlert, - loadAlertDetails, - handleOnAlertDetailsClosed, + onShowAlertDetails, }: ProcessTreeAlertDeps) => { const styles = useStyles({ isInvestigated, isSelected }); @@ -41,10 +39,10 @@ export const ProcessTreeAlert = ({ }, [isInvestigated, uuid, selectAlert]); const handleExpandClick = useCallback(() => { - if (loadAlertDetails && uuid) { - loadAlertDetails(uuid, () => handleOnAlertDetailsClosed(uuid)); + if (uuid) { + onShowAlertDetails(uuid); } - }, [handleOnAlertDetailsClosed, loadAlertDetails, uuid]); + }, [onShowAlertDetails, uuid]); const handleClick = useCallback(() => { if (alert.kibana?.alert) { diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx index 2333c71d36a51..ee6866f6a8a60 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx @@ -17,7 +17,7 @@ describe('ProcessTreeAlerts component', () => { const props: ProcessTreeAlertsDeps = { alerts: mockAlerts, onAlertSelected: jest.fn(), - handleOnAlertDetailsClosed: jest.fn(), + onShowAlertDetails: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx index c97ccfe253605..b51d58bf825ec 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx @@ -16,8 +16,7 @@ export interface ProcessTreeAlertsDeps { jumpToAlertID?: string; isProcessSelected?: boolean; onAlertSelected: (e: MouseEvent) => void; - loadAlertDetails?: (alertUuid: string, handleOnAlertDetailsClosed: () => void) => void; - handleOnAlertDetailsClosed: (alertUuid: string) => void; + onShowAlertDetails: (alertUuid: string) => void; } export function ProcessTreeAlerts({ @@ -25,8 +24,7 @@ export function ProcessTreeAlerts({ jumpToAlertID, isProcessSelected = false, onAlertSelected, - loadAlertDetails, - handleOnAlertDetailsClosed, + onShowAlertDetails, }: ProcessTreeAlertsDeps) { const [selectedAlert, setSelectedAlert] = useState(null); const styles = useStyles(); @@ -90,8 +88,7 @@ export function ProcessTreeAlerts({ isSelected={isProcessSelected && selectedAlert?.uuid === alertUuid} onClick={handleAlertClick} selectAlert={selectAlert} - loadAlertDetails={loadAlertDetails} - handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} + onShowAlertDetails={onShowAlertDetails} /> ); })} diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx index 2e82e822f0c82..5c3b790ad0430 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx @@ -36,7 +36,7 @@ describe('ProcessTreeNode component', () => { }, } as unknown as RefObject, onChangeJumpToEventVisibility: jest.fn(), - handleOnAlertDetailsClosed: (_alertUuid: string) => {}, + onShowAlertDetails: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx index fb878963f009a..387e7a5074699 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx @@ -44,8 +44,7 @@ export interface ProcessDeps { verboseModeOn?: boolean; scrollerRef: RefObject; onChangeJumpToEventVisibility: (isVisible: boolean, isAbove: boolean) => void; - loadAlertDetails?: (alertUuid: string, handleOnAlertDetailsClosed: () => void) => void; - handleOnAlertDetailsClosed: (alertUuid: string) => void; + onShowAlertDetails: (alertUuid: string) => void; } /** @@ -63,8 +62,7 @@ export function ProcessTreeNode({ verboseModeOn = true, scrollerRef, onChangeJumpToEventVisibility, - loadAlertDetails, - handleOnAlertDetailsClosed, + onShowAlertDetails, }: ProcessDeps) { const textRef = useRef(null); @@ -283,8 +281,7 @@ export function ProcessTreeNode({ jumpToAlertID={jumpToAlertID} isProcessSelected={selectedProcessId === process.id} onAlertSelected={onProcessClicked} - loadAlertDetails={loadAlertDetails} - handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} + onShowAlertDetails={onShowAlertDetails} /> )} @@ -304,8 +301,7 @@ export function ProcessTreeNode({ verboseModeOn={verboseModeOn} scrollerRef={scrollerRef} onChangeJumpToEventVisibility={onChangeJumpToEventVisibility} - loadAlertDetails={loadAlertDetails} - handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} + onShowAlertDetails={onShowAlertDetails} /> ); })} diff --git a/x-pack/plugins/session_view/public/components/session_view/hooks.ts b/x-pack/plugins/session_view/public/components/session_view/hooks.ts index ee300ba17e879..bf8796336602d 100644 --- a/x-pack/plugins/session_view/public/components/session_view/hooks.ts +++ b/x-pack/plugins/session_view/public/components/session_view/hooks.ts @@ -15,6 +15,7 @@ import { ProcessEventResults, } from '../../../common/types/process_tree'; import { + ALERTS_ROUTE, PROCESS_EVENTS_ROUTE, PROCESS_EVENTS_PER_PAGE, ALERT_STATUS_ROUTE, @@ -108,9 +109,9 @@ export const useFetchSessionViewAlerts = (sessionEntityId: string) => { refetchOnReconnect: false, } ); - + return query; -} +}; export const useFetchAlertStatus = ( updatedAlertsStatus: AlertStatusEventEntityIdMap, diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx index 18f3ec9194a5e..ee481c4204108 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -23,10 +23,10 @@ import { SessionViewDetailPanel } from '../session_view_detail_panel'; import { SessionViewSearchBar } from '../session_view_search_bar'; import { SessionViewDisplayOptions } from '../session_view_display_options'; import { useStyles } from './styles'; -import { +import { useFetchAlertStatus, useFetchSessionViewProcessEvents, - useFetchSessionViewAlerts + useFetchSessionViewAlerts, } from './hooks'; /** @@ -91,6 +91,15 @@ export const SessionView = ({ setIsDetailOpen(!isDetailOpen); }, [isDetailOpen]); + const onShowAlertDetails = useCallback( + (alertUuid: string) => { + if (loadAlertDetails) { + loadAlertDetails(alertUuid, () => handleOnAlertDetailsClosed(alertUuid)); + } + }, + [loadAlertDetails, handleOnAlertDetailsClosed] + ); + const handleOptionChange = useCallback((checkedOptions: DisplayOptionsState) => { setDisplayOptions(checkedOptions); }, []); @@ -213,8 +222,7 @@ export const SessionView = ({ fetchPreviousPage={fetchPreviousPage} setSearchResults={setSearchResults} updatedAlertsStatus={updatedAlertsStatus} - loadAlertDetails={loadAlertDetails} - handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} + onShowAlertDetails={onShowAlertDetails} timeStampOn={displayOptions.timestamp} verboseModeOn={displayOptions.verboseMode} /> @@ -237,7 +245,7 @@ export const SessionView = ({ investigatedAlert={jumpToEvent} selectedProcess={selectedProcess} onProcessSelected={onProcessSelected} - onShowAlertDetails={loadAlertDetails} + onShowAlertDetails={onShowAlertDetails} /> From 6ad0f9e622f62563eec952d860ff7220c9dbdfac Mon Sep 17 00:00:00 2001 From: mitodrummer Date: Tue, 22 Mar 2022 15:50:10 -0700 Subject: [PATCH 12/13] removed rule_registry ecs mappings. kqualters PR will add this. --- .../common/assets/field_maps/ecs_field_map.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts index c3e3aa5c604db..dc81e200032f7 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts @@ -2401,21 +2401,6 @@ export const ecsFieldMap = { array: false, required: false, }, - 'process.entry_leader.entity_id': { - type: 'keyword', - array: false, - required: false, - }, - 'process.session_leader.entity_id': { - type: 'keyword', - array: false, - required: false, - }, - 'process.group_leader.entity_id': { - type: 'keyword', - array: false, - required: false, - }, 'process.executable': { type: 'keyword', array: false, From 90cc8b0bf1354881a10cbf2dfd6f9a6492dbd58c Mon Sep 17 00:00:00 2001 From: mitodrummer Date: Tue, 22 Mar 2022 22:20:21 -0700 Subject: [PATCH 13/13] alerts index merge conflict fix --- x-pack/plugins/session_view/common/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/session_view/common/constants.ts b/x-pack/plugins/session_view/common/constants.ts index 41db0d7f5f807..9e8e1ae0d5e04 100644 --- a/x-pack/plugins/session_view/common/constants.ts +++ b/x-pack/plugins/session_view/common/constants.ts @@ -10,6 +10,7 @@ export const ALERTS_ROUTE = '/internal/session_view/alerts_route'; export const ALERT_STATUS_ROUTE = '/internal/session_view/alert_status_route'; export const SESSION_ENTRY_LEADERS_ROUTE = '/internal/session_view/session_entry_leaders_route'; export const PROCESS_EVENTS_INDEX = 'logs-endpoint.events.process-default'; +export const ALERTS_INDEX = '.alerts-security.alerts-default'; // TODO: changes to remove this and use AlertsClient instead to get indices. export const ENTRY_SESSION_ENTITY_ID_PROPERTY = 'process.entry_leader.entity_id'; export const ALERT_UUID_PROPERTY = 'kibana.alert.uuid'; export const KIBANA_DATE_FORMAT = 'MMM DD, YYYY @ hh:mm:ss.SSS';