From 2c5544cfc87cfa11800e4ab687ab39ec445b2d74 Mon Sep 17 00:00:00 2001 From: Kfir Peled <61654899+kfirpeled@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:17:05 +0000 Subject: [PATCH 1/5] [Cloud Security] Bug fix - show origin event's with primary color instead of danger (#204425) ## Summary Bug description: **Actual:** The node's color is red when exploring events through Explore or Timeline. **The expected** color of events is blue. Before: ![385007418-f0a6bd7e-dbc9-43ad-99b8-a07bcad85075](https://github.com/user-attachments/assets/7bf198f3-9a32-4d27-84db-3e97b5bf312b) After: https://github.com/user-attachments/assets/f1a10deb-65f5-43be-a351-6fca34f855cb https://github.com/user-attachments/assets/223534f4-09a2-4b41-85bc-c2195dd153ba **How to test this PR:** - Enable the feature flag `kibana.dev.yml`: ```yaml uiSettings.overrides.securitySolution:enableVisualizationsInFlyout: true xpack.securitySolution.enableExperimental: ['graphVisualizationInFlyoutEnabled'] ``` - Load mocked data: ```bash node scripts/es_archiver load x-pack/test/cloud_security_posture_functional/es_archives/logs_gcp_audit \ --es-url http://elastic:changeme@localhost:9200 \ --kibana-url http://elastic:changeme@localhost:5601 node scripts/es_archiver load x-pack/test/cloud_security_posture_functional/es_archives/security_alerts \ --es-url http://elastic:changeme@localhost:9200 \ --kibana-url http://elastic:changeme@localhost:5601 ``` - Make sure you include data from Oct 13 2024. (in the video I use Last year) ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --- .../common/schema/graph/v1.ts | 4 +- .../graph_investigation.tsx | 45 ++- .../src/hooks/use_fetch_graph_data.test.tsx | 8 +- .../graph/src/hooks/use_fetch_graph_data.ts | 6 +- .../server/routes/graph/route.ts | 4 +- .../server/routes/graph/v1.ts | 53 ++-- .../left/components/graph_visualization.tsx | 23 +- .../left/tabs/visualize_tab.tsx | 4 +- .../graph_preview_container.test.tsx | 117 +++++++- .../components/graph_preview_container.tsx | 5 +- .../visualizations_section.test.tsx | 1 + .../components/visualizations_section.tsx | 4 +- .../shared/hooks/use_graph_preview.test.tsx | 259 ++++++++++++------ .../shared/hooks/use_graph_preview.ts | 16 +- .../apis/cloud_security_posture/graph.ts | 2 +- .../test/cloud_security_posture_api/config.ts | 10 +- .../routes/graph.ts | 63 ++++- .../security/cloud_security_posture/graph.ts | 4 +- 18 files changed, 483 insertions(+), 145 deletions(-) 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: { From 1b52848c5b041bdd0500945cc4a1b98ef6360f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arturo=20Lidue=C3=B1a?= Date: Tue, 17 Dec 2024 17:33:40 +0100 Subject: [PATCH 2/5] [Observability] Update required field from 'score' to 'scores' (#203584) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #203739 ## Summary fix error: `“Error scoring documents: Tool call arguments for score were invalid”` [Observability] Update required field from 'score' to 'scores' in scoreSuggestions schema --- .../server/utils/recall/score_suggestions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/observability_solution/observability_ai_assistant/server/utils/recall/score_suggestions.ts b/x-pack/platform/plugins/shared/observability_solution/observability_ai_assistant/server/utils/recall/score_suggestions.ts index 7d1a19463cceb..b57e5928ce0ba 100644 --- a/x-pack/platform/plugins/shared/observability_solution/observability_ai_assistant/server/utils/recall/score_suggestions.ts +++ b/x-pack/platform/plugins/shared/observability_solution/observability_ai_assistant/server/utils/recall/score_suggestions.ts @@ -102,7 +102,7 @@ export async function scoreSuggestions({ type: 'string', }, }, - required: ['score'], + required: ['scores'], } as const, }; From cc34e97de4d2d8a5509eec5e4f9ffcb7338ed54a Mon Sep 17 00:00:00 2001 From: Faisal Kanout Date: Tue, 17 Dec 2024 17:45:16 +0100 Subject: [PATCH 3/5] Fix code scanning alert no. 494: Incomplete string escaping or encoding (#204528) Fixes [https://github.com/elastic/kibana/security/code-scanning/494](https://github.com/elastic/kibana/security/code-scanning/494) Fixes #1311 Escape first backslash occurrences and all single quotes in the `filter` string. --- .../public/components/rule_condition_chart/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/observability/plugins/observability/public/components/rule_condition_chart/helpers.ts b/x-pack/solutions/observability/plugins/observability/public/components/rule_condition_chart/helpers.ts index 30663d02cda72..775da2fc1c805 100644 --- a/x-pack/solutions/observability/plugins/observability/public/components/rule_condition_chart/helpers.ts +++ b/x-pack/solutions/observability/plugins/observability/public/components/rule_condition_chart/helpers.ts @@ -18,7 +18,7 @@ export const getLensOperationFromRuleMetric = (metric: GenericMetric): LensOpera const { aggType, field, filter = '' } = metric; let operation: string = aggType; const operationArgs: string[] = []; - const escapedFilter = filter.replace(/'/g, "\\'"); + const escapedFilter = filter.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); if (aggType === Aggregators.RATE) { return { From 02b65bd457293a71b730cf5ffbb376e637e2a7ff Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 17 Dec 2024 11:50:33 -0500 Subject: [PATCH 4/5] [Fleet] Remove adopted showDevToolRequest feature flag (#204586) --- .../fleet/common/experimental_features.ts | 1 - .../hooks/devtools_request.tsx | 8 +--- .../components/settings/index.tsx | 42 +++++++++---------- .../components/create_agent_policy.tsx | 36 +++++++--------- 4 files changed, 36 insertions(+), 51 deletions(-) diff --git a/x-pack/plugins/fleet/common/experimental_features.ts b/x-pack/plugins/fleet/common/experimental_features.ts index 1c42bf3ab2afe..1cf35952904aa 100644 --- a/x-pack/plugins/fleet/common/experimental_features.ts +++ b/x-pack/plugins/fleet/common/experimental_features.ts @@ -10,7 +10,6 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; const _allowedExperimentalValues = { createPackagePolicyMultiPageLayout: true, packageVerification: true, - showDevtoolsRequest: true, diagnosticFileUploadEnabled: true, displayAgentMetrics: true, showIntegrationsSubcategories: true, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/devtools_request.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/devtools_request.tsx index 91ca089f2ffe3..81f2304efee99 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/devtools_request.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/devtools_request.tsx @@ -11,7 +11,6 @@ import { i18n } from '@kbn/i18n'; import { omit } from 'lodash'; import { set } from '@kbn/safer-lodash-set'; -import { ExperimentalFeaturesService } from '../../../../../services'; import { generateCreatePackagePolicyDevToolsRequest, generateCreateAgentPolicyDevToolsRequest, @@ -39,12 +38,7 @@ export function useDevToolsRequest({ packageInfo?: PackageInfo; packagePolicyId?: string; }) { - const { showDevtoolsRequest: isShowDevtoolRequestExperimentEnabled } = - ExperimentalFeaturesService.get(); - - const showDevtoolsRequest = - !HIDDEN_API_REFERENCE_PACKAGES.includes(packageInfo?.name ?? '') && - isShowDevtoolRequestExperimentEnabled; + const showDevtoolsRequest = !HIDDEN_API_REFERENCE_PACKAGES.includes(packageInfo?.name ?? ''); const [devtoolRequest, devtoolRequestDescription] = useMemo(() => { if (selectedPolicyTab === SelectedPolicyTab.NEW) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx index 50baac84ff4a0..efb7066b0d5e0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx @@ -40,7 +40,6 @@ import { ConfirmDeployAgentPolicyModal, } from '../../../components'; import { DevtoolsRequestFlyoutButton } from '../../../../../components'; -import { ExperimentalFeaturesService } from '../../../../../services'; import { generateUpdateAgentPolicyDevToolsRequest } from '../../../services'; import { UNKNOWN_SPACE } from '../../../../../../../../common/constants'; @@ -155,7 +154,6 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( setIsLoading(false); }; - const { showDevtoolsRequest } = ExperimentalFeaturesService.get(); const devtoolRequest = useMemo( () => generateUpdateAgentPolicyDevToolsRequest( @@ -235,28 +233,26 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( /> - {showDevtoolsRequest ? ( - - 0 || - hasAdvancedSettingsErrors || - hasInvalidSpaceError + + 0 || + hasAdvancedSettingsErrors || + hasInvalidSpaceError + } + btnProps={{ + color: 'text', + }} + description={i18n.translate( + 'xpack.fleet.editAgentPolicy.devtoolsRequestDescription', + { + defaultMessage: 'This Kibana request updates an agent policy.', } - btnProps={{ - color: 'text', - }} - description={i18n.translate( - 'xpack.fleet.editAgentPolicy.devtoolsRequestDescription', - { - defaultMessage: 'This Kibana request updates an agent policy.', - } - )} - request={devtoolRequest} - /> - - ) : null} + )} + request={devtoolRequest} + /> + = ({ /> ); - const { showDevtoolsRequest } = ExperimentalFeaturesService.get(); const agentPolicyContent = useMemo( () => generateCreateAgentPolicyDevToolsRequest(agentPolicy, withSysMonitoring), [agentPolicy, withSysMonitoring] @@ -128,25 +126,23 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({ - {showDevtoolsRequest ? ( - - 0 || - hasAdvancedSettingsErrors || - hasInvalidSpaceError + + 0 || + hasAdvancedSettingsErrors || + hasInvalidSpaceError + } + description={i18n.translate( + 'xpack.fleet.createAgentPolicy.devtoolsRequestDescription', + { + defaultMessage: 'This Kibana request creates a new agent policy.', } - description={i18n.translate( - 'xpack.fleet.createAgentPolicy.devtoolsRequestDescription', - { - defaultMessage: 'This Kibana request creates a new agent policy.', - } - )} - request={agentPolicyContent} - /> - - ) : null} + )} + request={agentPolicyContent} + /> + Date: Tue, 17 Dec 2024 17:13:02 +0000 Subject: [PATCH 5/5] Fix share modal copy embed code (#204584) ## Summary Closes https://github.com/elastic/kibana/issues/204312 This PR removes an unnecessary function, that causes an infinite render loop for visualisations. ## After https://github.com/user-attachments/assets/259d238b-c1cf-4d74-bfca-1a6440d0f5cd ## How to test - From the left side nav, click the `Visualize Library` menu item, attempt to create a legacy visualisation, any one of choice and attempt sharing said created visualisation clicking the copy embed code should not result in any error but rather copy the embed code with the visual feedback. --- .../tabs/embed/embed_content.test.tsx | 7 +--- .../components/tabs/embed/embed_content.tsx | 6 ---- .../public/components/tabs/embed/index.tsx | 34 +------------------ 3 files changed, 2 insertions(+), 45 deletions(-) diff --git a/src/plugins/share/public/components/tabs/embed/embed_content.test.tsx b/src/plugins/share/public/components/tabs/embed/embed_content.test.tsx index ee9fdf01588df..edbb9ebb47995 100644 --- a/src/plugins/share/public/components/tabs/embed/embed_content.test.tsx +++ b/src/plugins/share/public/components/tabs/embed/embed_content.test.tsx @@ -18,12 +18,7 @@ describe('Share modal embed content tab', () => { beforeEach(() => { component = mountWithIntl( - jest.fn()} - shareableUrl="/home#/" - /> + ); }); diff --git a/src/plugins/share/public/components/tabs/embed/embed_content.tsx b/src/plugins/share/public/components/tabs/embed/embed_content.tsx index 5a9163097c8dc..a3bc6048e64e5 100644 --- a/src/plugins/share/public/components/tabs/embed/embed_content.tsx +++ b/src/plugins/share/public/components/tabs/embed/embed_content.tsx @@ -34,7 +34,6 @@ type EmbedProps = Pick< | 'objectType' | 'isDirty' > & { - setIsNotSaved: () => void; objectConfig?: ShareContextObjectTypeConfig; }; @@ -55,7 +54,6 @@ export const EmbedContent = ({ shareableUrl, objectType, objectConfig = {}, - setIsNotSaved, isDirty, }: EmbedProps) => { const isMounted = useMountedState(); @@ -67,10 +65,6 @@ export const EmbedContent = ({ const [anonymousAccessParameters] = useState(null); const [usePublicUrl] = useState(false); - useEffect(() => { - if (objectType !== 'dashboard') setIsNotSaved(); - }, [url, setIsNotSaved, objectType]); - const makeUrlEmbeddable = useCallback((tempUrl: string): string => { const embedParam = '?embed=true'; const urlHasQueryString = tempUrl.indexOf('?') !== -1; diff --git a/src/plugins/share/public/components/tabs/embed/index.tsx b/src/plugins/share/public/components/tabs/embed/index.tsx index 44d61833268cd..3c74ce3fe0b13 100644 --- a/src/plugins/share/public/components/tabs/embed/index.tsx +++ b/src/plugins/share/public/components/tabs/embed/index.tsx @@ -8,35 +8,13 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useCallback } from 'react'; +import React from 'react'; import { type IModalTabDeclaration } from '@kbn/shared-ux-tabbed-modal'; import { EmbedContent } from './embed_content'; import { useShareTabsContext } from '../../context'; -const EMBED_TAB_ACTIONS = { - SET_EMBED_URL: 'SET_EMBED_URL', - SET_IS_NOT_SAVED: 'SET_IS_NOT_SAVED', -}; - type IEmbedTab = IModalTabDeclaration<{ url: string; isNotSaved: boolean }>; -const embedTabReducer: IEmbedTab['reducer'] = (state = { url: '', isNotSaved: false }, action) => { - switch (action.type) { - case EMBED_TAB_ACTIONS.SET_IS_NOT_SAVED: - return { - ...state, - isNotSaved: action.payload, - }; - case EMBED_TAB_ACTIONS.SET_IS_NOT_SAVED: - return { - ...state, - isNotSaved: action.payload, - }; - default: - return state; - } -}; - const EmbedTabContent: NonNullable = ({ state, dispatch }) => { const { embedUrlParamExtensions, @@ -47,13 +25,6 @@ const EmbedTabContent: NonNullable = ({ state, dispatch }) isDirty, } = useShareTabsContext()!; - const setIsNotSaved = useCallback(() => { - dispatch({ - type: EMBED_TAB_ACTIONS.SET_IS_NOT_SAVED, - payload: objectType === 'dashboard' ? isDirty : false, - }); - }, [dispatch, objectType, isDirty]); - return ( = ({ state, dispatch }) shareableUrl, objectType, objectConfig: objectTypeMeta?.config?.embed, - isNotSaved: state?.isNotSaved, - setIsNotSaved, isDirty, }} /> @@ -75,6 +44,5 @@ export const embedTab: IEmbedTab = { name: i18n.translate('share.contextMenu.embedCodeTab', { defaultMessage: 'Embed', }), - reducer: embedTabReducer, content: EmbedTabContent, };