diff --git a/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/schema/graph/v1.ts b/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/schema/graph/v1.ts index 076c685aca5b9..5b1a48cf940b7 100644 --- a/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/schema/graph/v1.ts +++ b/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/schema/graph/v1.ts @@ -12,7 +12,9 @@ export const graphRequestSchema = schema.object({ nodesLimit: schema.maybe(schema.number()), showUnknownTarget: schema.maybe(schema.boolean()), query: schema.object({ - eventIds: schema.arrayOf(schema.string()), + originEventIds: schema.arrayOf( + schema.object({ id: schema.string(), isAlert: schema.boolean() }) + ), // TODO: use zod for range validation instead of config schema start: schema.oneOf([schema.number(), schema.string()]), end: schema.oneOf([schema.number(), schema.string()]), diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx index 081b4ec28c6a5..bd57082ba4cb9 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx @@ -126,21 +126,46 @@ const useGraphPopovers = ( }; interface GraphInvestigationProps { - dataView: DataView; - eventIds: string[]; - timestamp: string | null; + /** + * The initial state to use for the graph investigation view. + */ + initialState: { + /** + * The data view to use for the graph investigation view. + */ + dataView: DataView; + + /** + * The origin events for the graph investigation view. + */ + originEventIds: Array<{ + /** + * The ID of the origin event. + */ + id: string; + + /** + * A flag indicating whether the origin event is an alert or not. + */ + isAlert: boolean; + }>; + + /** + * The initial timerange for the graph investigation view. + */ + timeRange: TimeRange; + }; } /** * Graph investigation view allows the user to expand nodes and view related entities. */ export const GraphInvestigation: React.FC = memo( - ({ dataView, eventIds, timestamp = new Date().toISOString() }: GraphInvestigationProps) => { + ({ + initialState: { dataView, originEventIds, timeRange: initialTimeRange }, + }: GraphInvestigationProps) => { const [searchFilters, setSearchFilters] = useState(() => []); - const [timeRange, setTimeRange] = useState({ - from: `${timestamp}||-30m`, - to: `${timestamp}||+30m`, - }); + const [timeRange, setTimeRange] = useState(initialTimeRange); const { services: { uiSettings }, @@ -153,7 +178,7 @@ export const GraphInvestigation: React.FC = memo( [...searchFilters], getEsQueryConfig(uiSettings as Parameters[0]) ), - [searchFilters, dataView, uiSettings] + [dataView, searchFilters, uiSettings] ); const { nodeExpandPopover, openPopoverCallback } = useGraphPopovers( @@ -166,7 +191,7 @@ export const GraphInvestigation: React.FC = memo( const { data, refresh, isFetching } = useFetchGraphData({ req: { query: { - eventIds, + originEventIds, esQuery: query, start: timeRange.from, end: timeRange.to, diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.test.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.test.tsx index e494ff0957ecb..da5eaee9bfbf9 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.test.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.test.tsx @@ -33,7 +33,7 @@ describe('useFetchGraphData', () => { return useFetchGraphData({ req: { query: { - eventIds: [], + originEventIds: [], start: '2021-09-01T00:00:00.000Z', end: '2021-09-01T23:59:59.999Z', }, @@ -52,7 +52,7 @@ describe('useFetchGraphData', () => { return useFetchGraphData({ req: { query: { - eventIds: [], + originEventIds: [], start: '2021-09-01T00:00:00.000Z', end: '2021-09-01T23:59:59.999Z', }, @@ -75,7 +75,7 @@ describe('useFetchGraphData', () => { return useFetchGraphData({ req: { query: { - eventIds: [], + originEventIds: [], start: '2021-09-01T00:00:00.000Z', end: '2021-09-01T23:59:59.999Z', }, @@ -98,7 +98,7 @@ describe('useFetchGraphData', () => { return useFetchGraphData({ req: { query: { - eventIds: [], + originEventIds: [], start: '2021-09-01T00:00:00.000Z', end: '2021-09-01T23:59:59.999Z', }, diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.ts index 74cca4693e801..477492a3bbb7b 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.ts +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.ts @@ -81,13 +81,13 @@ export const useFetchGraphData = ({ options, }: UseFetchGraphDataParams): UseFetchGraphDataResult => { const queryClient = useQueryClient(); - const { esQuery, eventIds, start, end } = req.query; + const { esQuery, originEventIds, start, end } = req.query; const { services: { http }, } = useKibana(); const QUERY_KEY = useMemo( - () => ['useFetchGraphData', eventIds, start, end, esQuery], - [end, esQuery, eventIds, start] + () => ['useFetchGraphData', originEventIds, start, end, esQuery], + [end, esQuery, originEventIds, start] ); const { isLoading, isError, data, isFetching } = useQuery( diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/route.ts b/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/route.ts index f9544b656f927..f655747815850 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/route.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/route.ts @@ -43,7 +43,7 @@ export const defineGraphRoute = (router: CspRouter) => const cspContext = await context.csp; const { nodesLimit, showUnknownTarget = false } = request.body; - const { eventIds, start, end, esQuery } = request.body.query as GraphRequest['query']; + const { originEventIds, start, end, esQuery } = request.body.query as GraphRequest['query']; const spaceId = (await cspContext.spaces?.spacesService?.getActiveSpace(request))?.id; try { @@ -53,7 +53,7 @@ export const defineGraphRoute = (router: CspRouter) => esClient: cspContext.esClient, }, query: { - eventIds, + originEventIds, spaceId, start, end, diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/v1.ts b/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/v1.ts index b14a2ba3e06a9..d506bb856e766 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/v1.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/v1.ts @@ -33,7 +33,8 @@ interface GraphEdge { action: string; targetIds: string[] | string; eventOutcome: string; - isAlert: boolean; + isOrigin: boolean; + isOriginAlert: boolean; } interface LabelEdges { @@ -46,10 +47,15 @@ interface GraphContextServices { esClient: IScopedClusterClient; } +interface OriginEventId { + id: string; + isAlert: boolean; +} + interface GetGraphParams { services: GraphContextServices; query: { - eventIds: string[]; + originEventIds: OriginEventId[]; spaceId?: string; start: string | number; end: string | number; @@ -61,11 +67,13 @@ interface GetGraphParams { export const getGraph = async ({ services: { esClient, logger }, - query: { eventIds, spaceId = 'default', start, end, esQuery }, + query: { originEventIds, spaceId = 'default', start, end, esQuery }, showUnknownTarget, nodesLimit, }: GetGraphParams): Promise> => { - logger.trace(`Fetching graph for [eventIds: ${eventIds.join(', ')}] in [spaceId: ${spaceId}]`); + logger.trace( + `Fetching graph for [originEventIds: ${originEventIds.join(', ')}] in [spaceId: ${spaceId}]` + ); const results = await fetchGraph({ esClient, @@ -73,7 +81,7 @@ export const getGraph = async ({ logger, start, end, - eventIds, + originEventIds, esQuery, }); @@ -132,7 +140,7 @@ const fetchGraph = async ({ logger, start, end, - eventIds, + originEventIds, showUnknownTarget, esQuery, }: { @@ -140,15 +148,21 @@ const fetchGraph = async ({ logger: Logger; start: string | number; end: string | number; - eventIds: string[]; + originEventIds: OriginEventId[]; showUnknownTarget: boolean; esQuery?: EsQuery; }): Promise> => { + const originAlertIds = originEventIds.filter((originEventId) => originEventId.isAlert); const query = `from logs-* | WHERE event.action IS NOT NULL AND actor.entity.id IS NOT NULL -| EVAL isAlert = ${ - eventIds.length > 0 - ? `event.id in (${eventIds.map((_id, idx) => `?al_id${idx}`).join(', ')})` +| EVAL isOrigin = ${ + originEventIds.length > 0 + ? `event.id in (${originEventIds.map((_id, idx) => `?og_id${idx}`).join(', ')})` + : 'false' + } +| EVAL isOriginAlert = isOrigin AND ${ + originAlertIds.length > 0 + ? `event.id in (${originAlertIds.map((_id, idx) => `?og_alrt_id${idx}`).join(', ')})` : 'false' } | STATS badge = COUNT(*), @@ -159,19 +173,26 @@ const fetchGraph = async ({ action = event.action, targetIds = target.entity.id, eventOutcome = event.outcome, - isAlert + isOrigin, + isOriginAlert | LIMIT 1000 -| SORT isAlert DESC`; +| SORT isOrigin DESC`; logger.trace(`Executing query [${query}]`); + const eventIds = originEventIds.map((originEventId) => originEventId.id); return await esClient.asCurrentUser.helpers .esql({ columnar: false, filter: buildDslFilter(eventIds, showUnknownTarget, start, end, esQuery), query, // @ts-ignore - types are not up to date - params: [...eventIds.map((id, idx) => ({ [`al_id${idx}`]: id }))], + params: [ + ...originEventIds.map((originEventId, idx) => ({ [`og_id${idx}`]: originEventId.id })), + ...originEventIds + .filter((originEventId) => originEventId.isAlert) + .map((originEventId, idx) => ({ [`og_alrt_id${idx}`]: originEventId.id })), + ], }) .toRecords(); }; @@ -238,7 +259,7 @@ const createNodes = (records: GraphEdge[], context: Omit { const dataView = useGetScopedSourcererDataView({ sourcererScope: SourcererScopeName.default, }); - const { getFieldsData, dataAsNestedObject } = useDocumentDetailsContext(); - const { eventIds, timestamp } = useGraphPreview({ + const { getFieldsData, dataAsNestedObject, dataFormattedForFieldBrowser } = + useDocumentDetailsContext(); + const { + eventIds, + timestamp = new Date().toISOString(), + isAlert, + } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, + dataFormattedForFieldBrowser, }); + const originEventIds = eventIds.map((id) => ({ id, isAlert })); + return (
{ > {dataView && ( }> - + )}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx index 89e00e06e3a49..1a8e5906e247e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx @@ -88,7 +88,8 @@ const graphVisualizationButton: EuiButtonGroupOptionProps = { * Visualize view displayed in the document details expandable flyout left section */ export const VisualizeTab = memo(() => { - const { scopeId, getFieldsData, dataAsNestedObject } = useDocumentDetailsContext(); + const { scopeId, getFieldsData, dataAsNestedObject, dataFormattedForFieldBrowser } = + useDocumentDetailsContext(); const { openPreviewPanel } = useExpandableFlyoutApi(); const panels = useExpandableFlyoutState(); const [activeVisualizationId, setActiveVisualizationId] = useState( @@ -123,6 +124,7 @@ export const VisualizeTab = memo(() => { const { hasGraphRepresentation } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, + dataFormattedForFieldBrowser, }); const isGraphFeatureEnabled = useIsExperimentalFeatureEnabled( diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx index c805f2a3c67a7..9965c5300e71f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx @@ -86,6 +86,7 @@ describe('', () => { timestamp, eventIds: [], hasGraphRepresentation: true, + isAlert: true, }); const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview(); @@ -111,7 +112,109 @@ describe('', () => { expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ req: { query: { - eventIds: [], + originEventIds: [], + start: `${timestamp}||-30m`, + end: `${timestamp}||+30m`, + }, + }, + options: { + enabled: true, + refetchOnWindowFocus: false, + }, + }); + }); + + it('should render component for alert', async () => { + mockUseFetchGraphData.mockReturnValue({ + isLoading: false, + isError: false, + data: { nodes: DEFAULT_NODES, edges: [] }, + }); + + const timestamp = new Date().toISOString(); + + (useGraphPreview as jest.Mock).mockReturnValue({ + timestamp, + eventIds: ['eventId'], + isAlert: true, + hasGraphRepresentation: true, + }); + + const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview(); + + // Using findByTestId to wait for the component to be rendered because it is a lazy loaded component + expect(await findByTestId(GRAPH_PREVIEW_TEST_ID)).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + expect(mockUseFetchGraphData).toHaveBeenCalled(); + expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ + req: { + query: { + originEventIds: [{ id: 'eventId', isAlert: true }], + start: `${timestamp}||-30m`, + end: `${timestamp}||+30m`, + }, + }, + options: { + enabled: true, + refetchOnWindowFocus: false, + }, + }); + }); + + it('should render component for event', async () => { + mockUseFetchGraphData.mockReturnValue({ + isLoading: false, + isError: false, + data: { nodes: DEFAULT_NODES, edges: [] }, + }); + + const timestamp = new Date().toISOString(); + + (useGraphPreview as jest.Mock).mockReturnValue({ + timestamp, + eventIds: ['eventId'], + isAlert: false, + hasGraphRepresentation: true, + }); + + const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview(); + + // Using findByTestId to wait for the component to be rendered because it is a lazy loaded component + expect(await findByTestId(GRAPH_PREVIEW_TEST_ID)).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + expect(mockUseFetchGraphData).toHaveBeenCalled(); + expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ + req: { + query: { + originEventIds: [{ id: 'eventId', isAlert: false }], start: `${timestamp}||-30m`, end: `${timestamp}||+30m`, }, @@ -136,6 +239,7 @@ describe('', () => { timestamp, eventIds: [], hasGraphRepresentation: true, + isAlert: true, }); const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview({ @@ -164,7 +268,7 @@ describe('', () => { expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ req: { query: { - eventIds: [], + originEventIds: [], start: `${timestamp}||-30m`, end: `${timestamp}||+30m`, }, @@ -189,6 +293,7 @@ describe('', () => { timestamp, eventIds: [], hasGraphRepresentation: true, + isAlert: true, }); const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview({ @@ -217,7 +322,7 @@ describe('', () => { expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ req: { query: { - eventIds: [], + originEventIds: [], start: `${timestamp}||-30m`, end: `${timestamp}||+30m`, }, @@ -243,6 +348,7 @@ describe('', () => { timestamp, eventIds: [], hasGraphRepresentation: true, + isAlert: true, }); const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview(); @@ -268,7 +374,7 @@ describe('', () => { expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ req: { query: { - eventIds: [], + originEventIds: [], start: `${timestamp}||-30m`, end: `${timestamp}||+30m`, }, @@ -293,6 +399,7 @@ describe('', () => { timestamp, eventIds: [], hasGraphRepresentation: false, + isAlert: true, }); const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview(); @@ -320,7 +427,7 @@ describe('', () => { expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ req: { query: { - eventIds: [], + originEventIds: [], start: `${timestamp}||-30m`, end: `${timestamp}||+30m`, }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx index 90a0218778549..b4626f93e823d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx @@ -31,6 +31,7 @@ export const GraphPreviewContainer: React.FC = () => { scopeId, isPreview, isPreviewMode, + dataFormattedForFieldBrowser, } = useDocumentDetailsContext(); const [visualizationInFlyoutEnabled] = useUiSetting$( @@ -49,16 +50,18 @@ export const GraphPreviewContainer: React.FC = () => { eventIds, timestamp = new Date().toISOString(), hasGraphRepresentation, + isAlert, } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, + dataFormattedForFieldBrowser, }); // TODO: default start and end might not capture the original event const { isLoading, isError, data } = useFetchGraphData({ req: { query: { - eventIds, + originEventIds: eventIds.map((id) => ({ id, isAlert })), start: `${timestamp}||-30m`, end: `${timestamp}||+30m`, }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx index 6fb4d5d30b897..dc3b1f00e0d50 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx @@ -106,6 +106,7 @@ describe('', () => { }); mockUseGraphPreview.mockReturnValue({ hasGraphRepresentation: true, + eventIds: [], }); mockUseFetchGraphData.mockReturnValue({ isLoading: false, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx index 23bea1f8fecdd..467171cd49f2a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx @@ -28,7 +28,8 @@ const KEY = 'visualizations'; */ export const VisualizationsSection = memo(() => { const expanded = useExpandSection({ title: KEY, defaultValue: false }); - const { dataAsNestedObject, getFieldsData } = useDocumentDetailsContext(); + const { dataAsNestedObject, getFieldsData, dataFormattedForFieldBrowser } = + useDocumentDetailsContext(); const [visualizationInFlyoutEnabled] = useUiSetting$( ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING @@ -42,6 +43,7 @@ export const VisualizationsSection = memo(() => { const { hasGraphRepresentation } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, + dataFormattedForFieldBrowser, }); const shouldShowGraphPreview = diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.test.tsx index 453f897d4e188..cf1ee82078395 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.test.tsx @@ -5,15 +5,18 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react'; +import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import { renderHook } from '@testing-library/react'; -import type { UseGraphPreviewParams, UseGraphPreviewResult } from './use_graph_preview'; +import type { UseGraphPreviewParams } from './use_graph_preview'; import { useGraphPreview } from './use_graph_preview'; import type { GetFieldsData } from './use_get_fields_data'; import { mockFieldData } from '../mocks/mock_get_fields_data'; +import { mockDataFormattedForFieldBrowser } from '../mocks/mock_data_formatted_for_field_browser'; -const mockGetFieldsData: GetFieldsData = (field: string) => { - if (field === 'kibana.alert.original_event.id') { +const alertMockGetFieldsData: GetFieldsData = (field: string) => { + if (field === 'kibana.alert.uuid') { + return 'alertId'; + } else if (field === 'kibana.alert.original_event.id') { return 'eventId'; } else if (field === 'actor.entity.id') { return 'actorId'; @@ -24,18 +27,36 @@ const mockGetFieldsData: GetFieldsData = (field: string) => { return mockFieldData[field]; }; -describe('useGraphPreview', () => { - let hookResult: RenderHookResult; +const alertMockDataFormattedForFieldBrowser = mockDataFormattedForFieldBrowser; + +const eventMockGetFieldsData: GetFieldsData = (field: string) => { + if (field === 'kibana.alert.uuid') { + return; + } else if (field === 'kibana.alert.original_event.id') { + return; + } else if (field === 'event.id') { + return 'eventId'; + } else if (field === 'actor.entity.id') { + return 'actorId'; + } else if (field === 'target.entity.id') { + return 'targetId'; + } + return mockFieldData[field]; +}; + +const eventMockDataFormattedForFieldBrowser: TimelineEventsDetailsItem[] = []; + +describe('useGraphPreview', () => { it(`should return false when missing actor`, () => { const getFieldsData: GetFieldsData = (field: string) => { if (field === 'actor.entity.id') { return; } - return mockGetFieldsData(field); + return alertMockGetFieldsData(field); }; - hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { initialProps: { getFieldsData, ecsData: { @@ -44,37 +65,41 @@ describe('useGraphPreview', () => { action: ['action'], }, }, + dataFormattedForFieldBrowser: alertMockDataFormattedForFieldBrowser, }, }); - const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = - hookResult.result.current; - expect(hasGraphRepresentation).toEqual(false); - expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); - expect(eventIds).toEqual(['eventId']); - expect(actorIds).toEqual([]); - expect(targetIds).toEqual(['targetId']); - expect(action).toEqual(['action']); + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: false, + timestamp: mockFieldData['@timestamp'][0], + eventIds: ['eventId'], + actorIds: [], + action: ['action'], + targetIds: ['targetId'], + isAlert: true, + }); }); it(`should return false when missing event.action`, () => { - hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { initialProps: { - getFieldsData: mockGetFieldsData, + getFieldsData: alertMockGetFieldsData, ecsData: { _id: 'id', }, + dataFormattedForFieldBrowser: alertMockDataFormattedForFieldBrowser, }, }); - const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = - hookResult.result.current; - expect(hasGraphRepresentation).toEqual(false); - expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); - expect(eventIds).toEqual(['eventId']); - expect(actorIds).toEqual(['actorId']); - expect(targetIds).toEqual(['targetId']); - expect(action).toEqual(undefined); + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: false, + timestamp: mockFieldData['@timestamp'][0], + eventIds: ['eventId'], + actorIds: ['actorId'], + action: undefined, + targetIds: ['targetId'], + isAlert: true, + }); }); it(`should return false when missing target`, () => { @@ -82,26 +107,28 @@ describe('useGraphPreview', () => { if (field === 'target.entity.id') { return; } - return mockGetFieldsData(field); + return alertMockGetFieldsData(field); }; - hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { initialProps: { getFieldsData, ecsData: { _id: 'id', }, + dataFormattedForFieldBrowser: alertMockDataFormattedForFieldBrowser, }, }); - const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = - hookResult.result.current; - expect(hasGraphRepresentation).toEqual(false); - expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); - expect(eventIds).toEqual(['eventId']); - expect(actorIds).toEqual(['actorId']); - expect(targetIds).toEqual([]); - expect(action).toEqual(undefined); + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: false, + timestamp: mockFieldData['@timestamp'][0], + eventIds: ['eventId'], + actorIds: ['actorId'], + action: undefined, + targetIds: [], + isAlert: true, + }); }); it(`should return false when missing original_event.id`, () => { @@ -110,10 +137,10 @@ describe('useGraphPreview', () => { return; } - return mockGetFieldsData(field); + return alertMockGetFieldsData(field); }; - hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { initialProps: { getFieldsData, ecsData: { @@ -122,17 +149,19 @@ describe('useGraphPreview', () => { action: ['action'], }, }, + dataFormattedForFieldBrowser: alertMockDataFormattedForFieldBrowser, }, }); - const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = - hookResult.result.current; - expect(hasGraphRepresentation).toEqual(false); - expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); - expect(eventIds).toEqual([]); - expect(actorIds).toEqual(['actorId']); - expect(targetIds).toEqual(['targetId']); - expect(action).toEqual(['action']); + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: false, + timestamp: mockFieldData['@timestamp'][0], + eventIds: [], + actorIds: ['actorId'], + action: ['action'], + targetIds: ['targetId'], + isAlert: true, + }); }); it(`should return false when timestamp is missing`, () => { @@ -141,10 +170,10 @@ describe('useGraphPreview', () => { return; } - return mockGetFieldsData(field); + return alertMockGetFieldsData(field); }; - hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { initialProps: { getFieldsData, ecsData: { @@ -153,45 +182,53 @@ describe('useGraphPreview', () => { action: ['action'], }, }, + dataFormattedForFieldBrowser: alertMockDataFormattedForFieldBrowser, }, }); - const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = - hookResult.result.current; - expect(hasGraphRepresentation).toEqual(false); - expect(timestamp).toEqual(null); - expect(eventIds).toEqual(['eventId']); - expect(actorIds).toEqual(['actorId']); - expect(targetIds).toEqual(['targetId']); - expect(action).toEqual(['action']); + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: false, + timestamp: null, + eventIds: ['eventId'], + actorIds: ['actorId'], + action: ['action'], + targetIds: ['targetId'], + isAlert: true, + }); }); - it(`should return true when alert is has graph preview`, () => { - hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + it(`should return true when event has graph graph preview`, () => { + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { initialProps: { - getFieldsData: mockGetFieldsData, + getFieldsData: eventMockGetFieldsData, ecsData: { _id: 'id', event: { action: ['action'], }, }, + dataFormattedForFieldBrowser: eventMockDataFormattedForFieldBrowser, }, }); - const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = - hookResult.result.current; - expect(hasGraphRepresentation).toEqual(true); - expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); - expect(eventIds).toEqual(['eventId']); - expect(actorIds).toEqual(['actorId']); - expect(targetIds).toEqual(['targetId']); - expect(action).toEqual(['action']); + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: true, + timestamp: mockFieldData['@timestamp'][0], + eventIds: ['eventId'], + actorIds: ['actorId'], + action: ['action'], + targetIds: ['targetId'], + isAlert: false, + }); }); - it(`should return true when alert is has graph preview with multiple values`, () => { + it(`should return true when event has graph preview with multiple values`, () => { const getFieldsData: GetFieldsData = (field: string) => { - if (field === 'kibana.alert.original_event.id') { + if (field === 'kibana.alert.uuid') { + return; + } else if (field === 'kibana.alert.original_event.id') { + return; + } else if (field === 'event.id') { return ['id1', 'id2']; } else if (field === 'actor.entity.id') { return ['actorId1', 'actorId2']; @@ -202,7 +239,7 @@ describe('useGraphPreview', () => { return mockFieldData[field]; }; - hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { initialProps: { getFieldsData, ecsData: { @@ -211,16 +248,82 @@ describe('useGraphPreview', () => { action: ['action1', 'action2'], }, }, + dataFormattedForFieldBrowser: eventMockDataFormattedForFieldBrowser, }, }); - const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = - hookResult.result.current; - expect(hasGraphRepresentation).toEqual(true); - expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); - expect(eventIds).toEqual(['id1', 'id2']); - expect(actorIds).toEqual(['actorId1', 'actorId2']); - expect(action).toEqual(['action1', 'action2']); - expect(targetIds).toEqual(['targetId1', 'targetId2']); + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: true, + timestamp: mockFieldData['@timestamp'][0], + eventIds: ['id1', 'id2'], + actorIds: ['actorId1', 'actorId2'], + action: ['action1', 'action2'], + targetIds: ['targetId1', 'targetId2'], + isAlert: false, + }); + }); + + it(`should return true when alert has graph preview`, () => { + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + initialProps: { + getFieldsData: alertMockGetFieldsData, + ecsData: { + _id: 'id', + event: { + action: ['action'], + }, + }, + dataFormattedForFieldBrowser: alertMockDataFormattedForFieldBrowser, + }, + }); + + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: true, + timestamp: mockFieldData['@timestamp'][0], + eventIds: ['eventId'], + actorIds: ['actorId'], + action: ['action'], + targetIds: ['targetId'], + isAlert: true, + }); + }); + + it(`should return true when alert has graph preview with multiple values`, () => { + const getFieldsData: GetFieldsData = (field: string) => { + if (field === 'kibana.alert.uuid') { + return 'alertId'; + } else if (field === 'kibana.alert.original_event.id') { + return ['id1', 'id2']; + } else if (field === 'actor.entity.id') { + return ['actorId1', 'actorId2']; + } else if (field === 'target.entity.id') { + return ['targetId1', 'targetId2']; + } + + return mockFieldData[field]; + }; + + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + initialProps: { + getFieldsData, + ecsData: { + _id: 'id', + event: { + action: ['action1', 'action2'], + }, + }, + dataFormattedForFieldBrowser: alertMockDataFormattedForFieldBrowser, + }, + }); + + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: true, + timestamp: mockFieldData['@timestamp'][0], + eventIds: ['id1', 'id2'], + actorIds: ['actorId1', 'actorId2'], + action: ['action1', 'action2'], + targetIds: ['targetId1', 'targetId2'], + isAlert: true, + }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts index 48233afab02df..8f05b87844fb2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts @@ -5,10 +5,12 @@ * 2.0. */ +import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { get } from 'lodash/fp'; import type { GetFieldsData } from './use_get_fields_data'; import { getField, getFieldArray } from '../utils'; +import { useBasicDataFromDetailsData } from './use_basic_data_from_details_data'; export interface UseGraphPreviewParams { /** @@ -20,6 +22,11 @@ export interface UseGraphPreviewParams { * An object with top level fields from the ECS object */ ecsData: Ecs; + + /** + * An array of field objects with category and value + */ + dataFormattedForFieldBrowser: TimelineEventsDetailsItem[]; } /** * Interface for the result of the useGraphPreview hook @@ -54,6 +61,11 @@ export interface UseGraphPreviewResult { * Boolean indicating if the event is has a graph representation (contains event ids, actor ids and action) */ hasGraphRepresentation: boolean; + + /** + * Boolean indicating if the event is an alert or not + */ + isAlert: boolean; } /** @@ -62,6 +74,7 @@ export interface UseGraphPreviewResult { export const useGraphPreview = ({ getFieldsData, ecsData, + dataFormattedForFieldBrowser, }: UseGraphPreviewParams): UseGraphPreviewResult => { const timestamp = getField(getFieldsData('@timestamp')); const originalEventId = getFieldsData('kibana.alert.original_event.id'); @@ -77,6 +90,7 @@ export const useGraphPreview = ({ actorIds.length > 0 && eventIds.length > 0 && targetIds.length > 0; + const { isAlert } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); - return { timestamp, eventIds, actorIds, action, targetIds, hasGraphRepresentation }; + return { timestamp, eventIds, actorIds, action, targetIds, hasGraphRepresentation, isAlert }; }; diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts b/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts index 4ff483bff343d..4823c500a3588 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts @@ -40,7 +40,7 @@ export default function (providerContext: FtrProviderContext) { it('should return 404 when feature flag is not toggled', async () => { await postGraph(supertest, { query: { - eventIds: [], + originEventIds: [], start: 'now-1d/d', end: 'now/d', }, diff --git a/x-pack/test/cloud_security_posture_api/config.ts b/x-pack/test/cloud_security_posture_api/config.ts index 4e0ecd1f26e43..212abc50fc9ae 100644 --- a/x-pack/test/cloud_security_posture_api/config.ts +++ b/x-pack/test/cloud_security_posture_api/config.ts @@ -5,7 +5,7 @@ * 2.0. */ import { resolve } from 'path'; -import type { FtrConfigProviderContext } from '@kbn/test'; +import { getKibanaCliLoggers, type FtrConfigProviderContext } from '@kbn/test'; import { CLOUD_SECURITY_PLUGIN_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { @@ -21,6 +21,14 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...xPackAPITestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), + `--logging.loggers=${JSON.stringify([ + ...getKibanaCliLoggers(xPackAPITestsConfig.get('kbnTestServer.serverArgs')), + { + name: 'plugins.cloudSecurityPosture', + level: 'all', + appenders: ['default'], + }, + ])}`, /** * Package version is fixed (not latest) so FTR won't suddenly break when package is changed. * diff --git a/x-pack/test/cloud_security_posture_api/routes/graph.ts b/x-pack/test/cloud_security_posture_api/routes/graph.ts index 08adf73839ea2..e2be81a7d40e5 100644 --- a/x-pack/test/cloud_security_posture_api/routes/graph.ts +++ b/x-pack/test/cloud_security_posture_api/routes/graph.ts @@ -48,7 +48,7 @@ export default function (providerContext: FtrProviderContext) { supertestWithoutAuth, { query: { - eventIds: [], + originEventIds: [], start: 'now-1d/d', end: 'now/d', }, @@ -88,7 +88,7 @@ export default function (providerContext: FtrProviderContext) { it('should return 400 when missing `esQuery` field is not of type bool', async () => { await postGraph(supertest, { query: { - eventIds: [], + originEventIds: [], start: 'now-1d/d', end: 'now/d', esQuery: { @@ -102,7 +102,7 @@ export default function (providerContext: FtrProviderContext) { it('should return 400 with unsupported `esQuery`', async () => { await postGraph(supertest, { query: { - eventIds: [], + originEventIds: [], start: 'now-1d/d', end: 'now/d', esQuery: { @@ -122,7 +122,7 @@ export default function (providerContext: FtrProviderContext) { it('should return an empty graph / should return 200 when missing `esQuery` field', async () => { const response = await postGraph(supertest, { query: { - eventIds: [], + originEventIds: [], start: 'now-1d/d', end: 'now/d', }, @@ -136,7 +136,7 @@ export default function (providerContext: FtrProviderContext) { it('should return a graph with nodes and edges by actor', async () => { const response = await postGraph(supertest, { query: { - eventIds: [], + originEventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', esQuery: { @@ -177,7 +177,7 @@ export default function (providerContext: FtrProviderContext) { it('should return a graph with nodes and edges by alert', async () => { const response = await postGraph(supertest, { query: { - eventIds: ['kabcd1234efgh5678'], + originEventIds: [{ id: 'kabcd1234efgh5678', isAlert: true }], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', }, @@ -204,10 +204,40 @@ export default function (providerContext: FtrProviderContext) { }); }); + it('should return a graph with nodes and edges by origin event', async () => { + const response = await postGraph(supertest, { + query: { + originEventIds: [{ id: 'kabcd1234efgh5678', isAlert: false }], + start: '2024-09-01T00:00:00Z', + end: '2024-09-02T00:00:00Z', + }, + }).expect(result(200)); + + expect(response.body).to.have.property('nodes').length(3); + expect(response.body).to.have.property('edges').length(2); + expect(response.body).not.to.have.property('messages'); + + response.body.nodes.forEach((node: any) => { + expect(node).to.have.property('color'); + expect(node.color).equal( + 'primary', + `node color mismatched [node: ${node.id}] [actual: ${node.color}]` + ); + }); + + response.body.edges.forEach((edge: any) => { + expect(edge).to.have.property('color'); + expect(edge.color).equal( + 'primary', + `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` + ); + }); + }); + it('color of alert of failed event should be danger', async () => { const response = await postGraph(supertest, { query: { - eventIds: ['failed-event'], + originEventIds: [{ id: 'failed-event', isAlert: true }], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', }, @@ -237,7 +267,7 @@ export default function (providerContext: FtrProviderContext) { it('color of event of failed event should be warning', async () => { const response = await postGraph(supertest, { query: { - eventIds: [], + originEventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', esQuery: { @@ -279,7 +309,7 @@ export default function (providerContext: FtrProviderContext) { it('2 grouped events, 1 failed, 1 success', async () => { const response = await postGraph(supertest, { query: { - eventIds: [], + originEventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', esQuery: { @@ -327,7 +357,10 @@ export default function (providerContext: FtrProviderContext) { it('should support more than 1 eventIds', async () => { const response = await postGraph(supertest, { query: { - eventIds: ['kabcd1234efgh5678', 'failed-event'], + originEventIds: [ + { id: 'kabcd1234efgh5678', isAlert: true }, + { id: 'failed-event', isAlert: true }, + ], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', }, @@ -357,7 +390,7 @@ export default function (providerContext: FtrProviderContext) { it('should return a graph with nodes and edges by alert and actor', async () => { const response = await postGraph(supertest, { query: { - eventIds: ['kabcd1234efgh5678'], + originEventIds: [{ id: 'kabcd1234efgh5678', isAlert: true }], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', esQuery: { @@ -402,7 +435,7 @@ export default function (providerContext: FtrProviderContext) { it('should filter unknown targets', async () => { const response = await postGraph(supertest, { query: { - eventIds: [], + originEventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', esQuery: { @@ -428,7 +461,7 @@ export default function (providerContext: FtrProviderContext) { const response = await postGraph(supertest, { showUnknownTarget: true, query: { - eventIds: [], + originEventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', esQuery: { @@ -454,7 +487,7 @@ export default function (providerContext: FtrProviderContext) { const response = await postGraph(supertest, { nodesLimit: 1, query: { - eventIds: [], + originEventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', esQuery: { @@ -480,7 +513,7 @@ export default function (providerContext: FtrProviderContext) { it('should support date math', async () => { const response = await postGraph(supertest, { query: { - eventIds: ['kabcd1234efgh5678'], + originEventIds: [{ id: 'kabcd1234efgh5678', isAlert: true }], start: '2024-09-01T12:30:00.000Z||-30m', end: '2024-09-01T12:30:00.000Z||+30m', }, diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts index aaccdd0e9a41c..2cbe40e945492 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts @@ -49,7 +49,7 @@ export default function ({ getService }: FtrProviderContext) { it('should return an empty graph', async () => { const response = await postGraph(supertestViewer, { query: { - eventIds: [], + originEventIds: [], start: 'now-1d/d', end: 'now/d', }, @@ -63,7 +63,7 @@ export default function ({ getService }: FtrProviderContext) { it('should return a graph with nodes and edges by actor', async () => { const response = await postGraph(supertestViewer, { query: { - eventIds: [], + originEventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', esQuery: {