From 14363e870036608e4697a5740243439060305e87 Mon Sep 17 00:00:00 2001 From: Kfir Peled <61654899+kfirpeled@users.noreply.github.com> Date: Wed, 13 Nov 2024 01:43:58 +0000 Subject: [PATCH] [Cloud Security] Fixed failing FTR (#199683) ## Summary fixes: https://github.com/elastic/kibana/issues/198632 FTR failed due to the usage of relative start and end when querying for the graph. Together with @tinnytintin10 we decided it is safe to assume the time to query would be 30 minutes before and after the alert. **Some enhancements and fixes:** - Disabled re-fetch /graph API when window focus returned ### Checklist Delete any items that are not applicable to this PR. - [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 - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --- .../graph_preview_container.test.tsx | 35 ++++++---- .../components/graph_preview_container.tsx | 67 +++++++++--------- .../right/hooks/use_graph_preview.test.tsx | 70 +++++++++++++++++-- .../right/hooks/use_graph_preview.ts | 17 +++-- .../routes/graph.ts | 20 +++++- .../es_archives/security_alerts/data.json | 36 +++++----- .../pages/alerts_flyout.ts | 3 +- 7 files changed, 169 insertions(+), 79 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx index d44321a4926bd..ae907af316dc9 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx @@ -28,15 +28,6 @@ jest.mock('../hooks/use_fetch_graph_data', () => ({ })); const mockUseFetchGraphData = useFetchGraphData as jest.Mock; -const mockUseUiSetting = jest.fn().mockReturnValue([false]); -jest.mock('@kbn/kibana-react-plugin/public', () => { - const original = jest.requireActual('@kbn/kibana-react-plugin/public'); - return { - ...original, - useUiSetting$: () => mockUseUiSetting(), - }; -}); - const mockGraph = () =>
; jest.mock('@kbn/cloud-security-posture-graph', () => { @@ -64,7 +55,11 @@ describe('', () => { data: { nodes: [], edges: [] }, }); + const timestamp = new Date().toISOString(); + (useGraphPreview as jest.Mock).mockReturnValue({ + timestamp, + eventIds: [], isAuditLog: true, }); @@ -87,9 +82,23 @@ describe('', () => { expect( getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) ).toBeInTheDocument(); + expect(mockUseFetchGraphData).toHaveBeenCalled(); + expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ + req: { + query: { + eventIds: [], + start: `${timestamp}||-30m`, + end: `${timestamp}||+30m`, + }, + }, + options: { + enabled: true, + refetchOnWindowFocus: false, + }, + }); }); - it('should render error message and text in header', () => { + it('should not render when graph data is not available', () => { mockUseFetchGraphData.mockReturnValue({ isLoading: false, isError: false, @@ -100,10 +109,10 @@ describe('', () => { isAuditLog: false, }); - const { getByTestId } = renderGraphPreview(); + const { queryByTestId } = renderGraphPreview(); expect( - getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) - ).toBeInTheDocument(); + queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx index af9e8dca1f24f..0b881b8f8d439 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx @@ -14,57 +14,60 @@ import { useFetchGraphData } from '../hooks/use_fetch_graph_data'; import { useGraphPreview } from '../hooks/use_graph_preview'; import { ExpandablePanel } from '../../../shared/components/expandable_panel'; -const DEFAULT_FROM = 'now-60d/d'; -const DEFAULT_TO = 'now/d'; - /** * Graph preview under Overview, Visualizations. It shows a graph representation of entities. */ export const GraphPreviewContainer: React.FC = () => { const { dataAsNestedObject, getFieldsData } = useDocumentDetailsContext(); - const { eventIds } = useGraphPreview({ + const { + eventIds, + timestamp = new Date().toISOString(), + isAuditLog, + } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, }); // TODO: default start and end might not capture the original event - const graphFetchQuery = useFetchGraphData({ + const { isLoading, isError, data } = useFetchGraphData({ req: { query: { eventIds, - start: DEFAULT_FROM, - end: DEFAULT_TO, + start: `${timestamp}||-30m`, + end: `${timestamp}||+30m`, }, }, + options: { + enabled: isAuditLog, + refetchOnWindowFocus: false, + }, }); return ( - - ), - iconType: 'indexMapping', - }} - data-test-subj={GRAPH_PREVIEW_TEST_ID} - content={ - !graphFetchQuery.isLoading && !graphFetchQuery.isError - ? { - paddingSize: 'none', - } - : undefined - } - > - - + isAuditLog && ( + + ), + iconType: 'indexMapping', + }} + data-test-subj={GRAPH_PREVIEW_TEST_ID} + content={ + !isLoading && !isError + ? { + paddingSize: 'none', + } + : undefined + } + > + + + ) ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.test.tsx index ff6118ec9b743..d12154a390abf 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.test.tsx @@ -18,7 +18,7 @@ describe('useGraphPreview', () => { it(`should return false when missing actor`, () => { const getFieldsData: GetFieldsData = (field: string) => { if (field === 'kibana.alert.original_event.id') { - return field; + return 'eventId'; } return mockFieldData[field]; }; @@ -35,7 +35,12 @@ describe('useGraphPreview', () => { }, }); - expect(hookResult.result.current.isAuditLog).toEqual(false); + const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current; + expect(isAuditLog).toEqual(false); + expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); + expect(eventIds).toEqual(['eventId']); + expect(actorIds).toEqual([]); + expect(action).toEqual(['action']); }); it(`should return false when missing event.action`, () => { @@ -57,7 +62,12 @@ describe('useGraphPreview', () => { }, }); - expect(hookResult.result.current.isAuditLog).toEqual(false); + const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current; + expect(isAuditLog).toEqual(false); + expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); + expect(eventIds).toEqual(['eventId']); + expect(actorIds).toEqual(['actorId']); + expect(action).toEqual(undefined); }); it(`should return false when missing original_event.id`, () => { @@ -80,7 +90,45 @@ describe('useGraphPreview', () => { }, }); - expect(hookResult.result.current.isAuditLog).toEqual(false); + const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current; + expect(isAuditLog).toEqual(false); + expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); + expect(eventIds).toEqual([]); + expect(actorIds).toEqual(['actorId']); + expect(action).toEqual(['action']); + }); + + it(`should return false when timestamp is missing`, () => { + const getFieldsData: GetFieldsData = (field: string) => { + if (field === '@timestamp') { + return; + } else if (field === 'kibana.alert.original_event.id') { + return 'eventId'; + } else if (field === 'actor.entity.id') { + return 'actorId'; + } + + return mockFieldData[field]; + }; + + hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + initialProps: { + getFieldsData, + ecsData: { + _id: 'id', + event: { + action: ['action'], + }, + }, + }, + }); + + const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current; + expect(isAuditLog).toEqual(false); + expect(timestamp).toEqual(null); + expect(eventIds).toEqual(['eventId']); + expect(actorIds).toEqual(['actorId']); + expect(action).toEqual(['action']); }); it(`should return true when alert is has graph preview`, () => { @@ -106,7 +154,12 @@ describe('useGraphPreview', () => { }, }); - expect(hookResult.result.current.isAuditLog).toEqual(true); + const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current; + expect(isAuditLog).toEqual(true); + expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); + expect(eventIds).toEqual(['eventId']); + expect(actorIds).toEqual(['actorId']); + expect(action).toEqual(['action']); }); it(`should return true when alert is has graph preview with multiple values`, () => { @@ -132,6 +185,11 @@ describe('useGraphPreview', () => { }, }); - expect(hookResult.result.current.isAuditLog).toEqual(true); + const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current; + expect(isAuditLog).toEqual(true); + expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); + expect(eventIds).toEqual(['id1', 'id2']); + expect(actorIds).toEqual(['actorId1', 'actorId2']); + expect(action).toEqual(['action1', 'action2']); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.ts index d833c0aa86dbc..bbaeb808c9e2a 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.ts @@ -8,7 +8,7 @@ import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { get } from 'lodash/fp'; import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data'; -import { getFieldArray } from '../../shared/utils'; +import { getField, getFieldArray } from '../../shared/utils'; export interface UseGraphPreviewParams { /** @@ -25,6 +25,11 @@ export interface UseGraphPreviewParams { * Interface for the result of the useGraphPreview hook */ export interface UseGraphPreviewResult { + /** + * The timestamp of the event + */ + timestamp: string | null; + /** * Array of event IDs associated with the alert */ @@ -38,7 +43,7 @@ export interface UseGraphPreviewResult { /** * Action associated with the event */ - action: string | undefined; + action?: string[]; /** * Boolean indicating if the event is an audit log (contains event ids, actor ids and action) @@ -53,13 +58,15 @@ export const useGraphPreview = ({ getFieldsData, ecsData, }: UseGraphPreviewParams): UseGraphPreviewResult => { + const timestamp = getField(getFieldsData('@timestamp')); const originalEventId = getFieldsData('kibana.alert.original_event.id'); const eventId = getFieldsData('event.id'); const eventIds = originalEventId ? getFieldArray(originalEventId) : getFieldArray(eventId); const actorIds = getFieldArray(getFieldsData('actor.entity.id')); - const action = get(['event', 'action'], ecsData); - const isAuditLog = actorIds.length > 0 && action?.length > 0 && eventIds.length > 0; + const action: string[] | undefined = get(['event', 'action'], ecsData); + const isAuditLog = + Boolean(timestamp) && actorIds.length > 0 && Boolean(action?.length) && eventIds.length > 0; - return { eventIds, actorIds, action, isAuditLog }; + return { timestamp, eventIds, actorIds, action, isAuditLog }; }; 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 8043e6e22feb6..95625b24fa59a 100644 --- a/x-pack/test/cloud_security_posture_api/routes/graph.ts +++ b/x-pack/test/cloud_security_posture_api/routes/graph.ts @@ -399,7 +399,7 @@ export default function (providerContext: FtrProviderContext) { }); }); - it('Should filter unknown targets', async () => { + it('should filter unknown targets', async () => { const response = await postGraph(supertest, { query: { eventIds: [], @@ -424,7 +424,7 @@ export default function (providerContext: FtrProviderContext) { expect(response.body).not.to.have.property('messages'); }); - it('Should return unknown targets', async () => { + it('should return unknown targets', async () => { const response = await postGraph(supertest, { showUnknownTarget: true, query: { @@ -450,7 +450,7 @@ export default function (providerContext: FtrProviderContext) { expect(response.body).not.to.have.property('messages'); }); - it('Should limit number of nodes', async () => { + it('should limit number of nodes', async () => { const response = await postGraph(supertest, { nodesLimit: 1, query: { @@ -476,6 +476,20 @@ export default function (providerContext: FtrProviderContext) { expect(response.body).to.have.property('messages').length(1); expect(response.body.messages[0]).equal(ApiMessageCode.ReachedNodesLimit); }); + + it('should support date math', async () => { + const response = await postGraph(supertest, { + query: { + eventIds: ['kabcd1234efgh5678'], + start: '2024-09-01T12:30:00.000Z||-30m', + end: '2024-09-01T12:30:00.000Z||+30m', + }, + }).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'); + }); }); }); } diff --git a/x-pack/test/cloud_security_posture_functional/es_archives/security_alerts/data.json b/x-pack/test/cloud_security_posture_functional/es_archives/security_alerts/data.json index 94ecc85bfd234..e28c92931b619 100644 --- a/x-pack/test/cloud_security_posture_functional/es_archives/security_alerts/data.json +++ b/x-pack/test/cloud_security_posture_functional/es_archives/security_alerts/data.json @@ -4,7 +4,7 @@ "id": "589e086d7ceec7d4b353340578bd607e96fbac7eab9e2926f110990be15122f1", "index": ".internal.alerts-security.alerts-default-000001", "source": { - "@timestamp": "2024-09-01T20:44:02.109Z", + "@timestamp": "2024-09-01T12:44:02.109Z", "actor": { "entity": { "id": "admin@example.com" @@ -34,7 +34,7 @@ ], "dataset": "gcp.audit", "id": "kabcd1234efgh5678", - "ingested": "2024-09-01T20:40:17Z", + "ingested": "2024-09-01T12:40:17Z", "module": "gcp", "outcome": "success", "provider": "activity", @@ -95,8 +95,8 @@ } ], "kibana.alert.depth": 1, - "kibana.alert.intended_timestamp": "2024-09-01T20:44:02.117Z", - "kibana.alert.last_detected": "2024-09-01T20:44:02.117Z", + "kibana.alert.intended_timestamp": "2024-09-01T12:44:02.117Z", + "kibana.alert.last_detected": "2024-09-01T12:44:02.117Z", "kibana.alert.original_event.action": "google.iam.admin.v1.CreateRole", "kibana.alert.original_event.agent_id_status": "missing", "kibana.alert.original_event.category": [ @@ -106,7 +106,7 @@ ], "kibana.alert.original_event.dataset": "gcp.audit", "kibana.alert.original_event.id": "kabcd1234efgh5678", - "kibana.alert.original_event.ingested": "2024-09-01T20:40:17Z", + "kibana.alert.original_event.ingested": "2024-09-01T12:40:17Z", "kibana.alert.original_event.kind": "event", "kibana.alert.original_event.module": "gcp", "kibana.alert.original_event.outcome": "success", @@ -126,13 +126,13 @@ ], "kibana.alert.rule.category": "Custom Query Rule", "kibana.alert.rule.consumer": "siem", - "kibana.alert.rule.created_at": "2024-09-01T20:38:49.650Z", + "kibana.alert.rule.created_at": "2024-09-01T12:38:49.650Z", "kibana.alert.rule.created_by": "elastic", "kibana.alert.rule.description": "Identifies an Identity and Access Management (IAM) custom role creation in Google Cloud Platform (GCP). Custom roles are user-defined, and allow for the bundling of one or more supported permissions to meet specific needs. Custom roles will not be updated automatically and could lead to privilege creep if not carefully scrutinized.", "kibana.alert.rule.enabled": true, "kibana.alert.rule.exceptions_list": [ ], - "kibana.alert.rule.execution.timestamp": "2024-09-01T20:44:02.117Z", + "kibana.alert.rule.execution.timestamp": "2024-09-01T12:44:02.117Z", "kibana.alert.rule.execution.uuid": "a440f349-1900-4087-b507-f2b98c6cfa79", "kibana.alert.rule.false_positives": [ "Custom role creations may be done by a system or network administrator. Verify whether the user email, resource name, and/or hostname should be making changes in your environment. Role creations by unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule." @@ -300,12 +300,12 @@ "kibana.alert.rule.timestamp_override": "event.ingested", "kibana.alert.rule.to": "now", "kibana.alert.rule.type": "query", - "kibana.alert.rule.updated_at": "2024-09-01T20:39:00.099Z", + "kibana.alert.rule.updated_at": "2024-09-01T12:39:00.099Z", "kibana.alert.rule.updated_by": "elastic", "kibana.alert.rule.uuid": "c6f64115-5941-46ef-bfa3-61a4ecb4f3ba", "kibana.alert.rule.version": 104, "kibana.alert.severity": "medium", - "kibana.alert.start": "2024-09-01T20:44:02.117Z", + "kibana.alert.start": "2024-09-01T12:44:02.117Z", "kibana.alert.status": "active", "kibana.alert.uuid": "589e086d7ceec7d4b353340578bd607e96fbac7eab9e2926f110990be15122f1", "kibana.alert.workflow_assignee_ids": [ @@ -361,7 +361,7 @@ "id": "838ea37ab43ab7d2754d007fbe8191be53d7d637bea62f6189f8db1503c0e250", "index": ".internal.alerts-security.alerts-default-000001", "source": { - "@timestamp": "2024-09-01T20:39:03.646Z", + "@timestamp": "2024-09-01T12:39:03.646Z", "actor": { "entity": { "id": "admin@example.com" @@ -391,7 +391,7 @@ ], "dataset": "gcp.audit", "id": "kabcd1234efgh5678", - "ingested": "2024-09-01T20:38:13Z", + "ingested": "2024-09-01T12:38:13Z", "module": "gcp", "outcome": "success", "provider": "activity", @@ -452,8 +452,8 @@ } ], "kibana.alert.depth": 1, - "kibana.alert.intended_timestamp": "2024-09-01T20:39:03.657Z", - "kibana.alert.last_detected": "2024-09-01T20:39:03.657Z", + "kibana.alert.intended_timestamp": "2024-09-01T12:39:03.657Z", + "kibana.alert.last_detected": "2024-09-01T12:39:03.657Z", "kibana.alert.original_event.action": "google.iam.admin.v1.CreateRole", "kibana.alert.original_event.agent_id_status": "missing", "kibana.alert.original_event.category": [ @@ -463,7 +463,7 @@ ], "kibana.alert.original_event.dataset": "gcp.audit", "kibana.alert.original_event.id": "kabcd1234efgh5678", - "kibana.alert.original_event.ingested": "2024-09-01T20:38:13Z", + "kibana.alert.original_event.ingested": "2024-09-01T12:38:13Z", "kibana.alert.original_event.kind": "event", "kibana.alert.original_event.module": "gcp", "kibana.alert.original_event.outcome": "success", @@ -483,13 +483,13 @@ ], "kibana.alert.rule.category": "Custom Query Rule", "kibana.alert.rule.consumer": "siem", - "kibana.alert.rule.created_at": "2024-09-01T20:38:49.650Z", + "kibana.alert.rule.created_at": "2024-09-01T12:38:49.650Z", "kibana.alert.rule.created_by": "elastic", "kibana.alert.rule.description": "Identifies an Identity and Access Management (IAM) custom role creation in Google Cloud Platform (GCP). Custom roles are user-defined, and allow for the bundling of one or more supported permissions to meet specific needs. Custom roles will not be updated automatically and could lead to privilege creep if not carefully scrutinized.", "kibana.alert.rule.enabled": true, "kibana.alert.rule.exceptions_list": [ ], - "kibana.alert.rule.execution.timestamp": "2024-09-01T20:39:03.657Z", + "kibana.alert.rule.execution.timestamp": "2024-09-01T12:39:03.657Z", "kibana.alert.rule.execution.uuid": "939d34e1-1e74-480d-90ae-24079d9b40d3", "kibana.alert.rule.false_positives": [ "Custom role creations may be done by a system or network administrator. Verify whether the user email, resource name, and/or hostname should be making changes in your environment. Role creations by unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule." @@ -657,12 +657,12 @@ "kibana.alert.rule.timestamp_override": "event.ingested", "kibana.alert.rule.to": "now", "kibana.alert.rule.type": "query", - "kibana.alert.rule.updated_at": "2024-09-01T20:39:00.099Z", + "kibana.alert.rule.updated_at": "2024-09-01T12:39:00.099Z", "kibana.alert.rule.updated_by": "elastic", "kibana.alert.rule.uuid": "c6f64115-5941-46ef-bfa3-61a4ecb4f3ba", "kibana.alert.rule.version": 104, "kibana.alert.severity": "medium", - "kibana.alert.start": "2024-09-01T20:39:03.657Z", + "kibana.alert.start": "2024-09-01T12:39:03.657Z", "kibana.alert.status": "active", "kibana.alert.uuid": "838ea37ab43ab7d2754d007fbe8191be53d7d637bea62f6189f8db1503c0e250", "kibana.alert.workflow_assignee_ids": [ diff --git a/x-pack/test/cloud_security_posture_functional/pages/alerts_flyout.ts b/x-pack/test/cloud_security_posture_functional/pages/alerts_flyout.ts index a76ce4666d89f..63eafc4107bc1 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/alerts_flyout.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/alerts_flyout.ts @@ -17,8 +17,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const pageObjects = getPageObjects(['common', 'header', 'alerts']); const alertsPage = pageObjects.alerts; - // Failing: See https://github.com/elastic/kibana/issues/198632 - describe.skip('Security Alerts Page - Graph visualization', function () { + describe('Security Alerts Page - Graph visualization', function () { this.tags(['cloud_security_posture_graph_viz']); before(async () => {