From 1a99cc5ca98e80148e58958b4604c0c450980762 Mon Sep 17 00:00:00 2001 From: PhilippeOberti Date: Fri, 27 Dec 2024 16:28:29 -0600 Subject: [PATCH] [Security Solution][Alert details] - bring back last alert status change to flyout --- .../right/components/about_section.test.tsx | 3 + .../right/components/about_section.tsx | 2 + .../right/components/alert_status.test.tsx | 66 +++++++++++++++++++ .../right/components/alert_status.tsx | 61 +++++++++++++++++ .../right/components/test_ids.ts | 4 ++ .../alert_details_right_panel.cy.ts | 29 ++++++++ .../alert_details_right_panel_overview_tab.ts | 4 ++ 7 files changed, 169 insertions(+) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_status.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_status.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/about_section.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/about_section.test.tsx index 46cefbb7533aa..fd29a9d9b2636 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/about_section.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/about_section.test.tsx @@ -16,6 +16,7 @@ import { REASON_TITLE_TEST_ID, MITRE_ATTACK_TITLE_TEST_ID, EVENT_RENDERER_TEST_ID, + WORKFLOW_STATUS_TITLE_TEST_ID, } from './test_ids'; import { TestProviders } from '../../../../common/mock'; import { AboutSection } from './about_section'; @@ -106,6 +107,7 @@ describe('', () => { expect(queryByTestId(ALERT_DESCRIPTION_TITLE_TEST_ID)).not.toBeInTheDocument(); expect(queryByTestId(REASON_TITLE_TEST_ID)).not.toBeInTheDocument(); expect(queryByTestId(MITRE_ATTACK_TITLE_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(WORKFLOW_STATUS_TITLE_TEST_ID)).not.toBeInTheDocument(); expect(getByTestId(EVENT_KIND_DESCRIPTION_TEST_ID)).toBeInTheDocument(); @@ -135,6 +137,7 @@ describe('', () => { expect(queryByTestId(ALERT_DESCRIPTION_TITLE_TEST_ID)).not.toBeInTheDocument(); expect(queryByTestId(REASON_TITLE_TEST_ID)).not.toBeInTheDocument(); expect(queryByTestId(MITRE_ATTACK_TITLE_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(WORKFLOW_STATUS_TITLE_TEST_ID)).not.toBeInTheDocument(); expect(queryByTestId(EVENT_KIND_DESCRIPTION_TEST_ID)).not.toBeInTheDocument(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/about_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/about_section.tsx index 5b9da45df2dfd..9e2ddd858a42e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/about_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/about_section.tsx @@ -20,6 +20,7 @@ import { isEcsAllowedValue } from '../utils/event_utils'; import { EventCategoryDescription } from './event_category_description'; import { EventKindDescription } from './event_kind_description'; import { EventRenderer } from './event_renderer'; +import { AlertStatus } from './alert_status'; const KEY = 'about'; @@ -42,6 +43,7 @@ export const AboutSection = memo(() => { + ) : ( <> diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_status.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_status.test.tsx new file mode 100644 index 0000000000000..5c2353afd8833 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_status.test.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { act, render } from '@testing-library/react'; +import { AlertStatus } from './alert_status'; +import { mockContextValue } from '../../shared/mocks/mock_context'; +import { DocumentDetailsContext } from '../../shared/context'; +import { WORKFLOW_STATUS_DETAILS_TEST_ID, WORKFLOW_STATUS_TITLE_TEST_ID } from './test_ids'; +import { TestProviders } from '../../../../common/mock'; +import { useBulkGetUserProfiles } from '../../../../common/components/user_profiles/use_bulk_get_user_profiles'; + +jest.mock('../../../../common/components/user_profiles/use_bulk_get_user_profiles'); + +const renderAlertStatus = (contextValue: DocumentDetailsContext) => + render( + + + + + + ); + +const mockUserProfiles = [ + { uid: 'user-id-1', enabled: true, user: { username: 'user1', full_name: 'User 1' }, data: {} }, +]; + +describe('', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render alert status history information', async () => { + (useBulkGetUserProfiles as jest.Mock).mockReturnValue({ + isLoading: false, + data: mockUserProfiles, + }); + const contextValue = { + ...mockContextValue, + getFieldsData: jest.fn().mockImplementation((field: string) => { + if (field === 'kibana.alert.workflow_user') return ['user-id-1']; + if (field === 'kibana.alert.workflow_status_updated_at') + return ['2023-11-01T22:33:26.893Z']; + }), + }; + + const { getByTestId } = renderAlertStatus(contextValue); + + await act(async () => { + expect(getByTestId(WORKFLOW_STATUS_TITLE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(WORKFLOW_STATUS_DETAILS_TEST_ID)).toBeInTheDocument(); + }); + }); + + it('should render empty component if missing workflow_user value', async () => { + const { container } = renderAlertStatus(mockContextValue); + + await act(async () => { + expect(container).toBeEmptyDOMElement(); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_status.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_status.tsx new file mode 100644 index 0000000000000..a4e8c2ea3f484 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_status.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { getUserDisplayName } from '@kbn/user-profile-components'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { FC } from 'react'; +import React from 'react'; +import { WORKFLOW_STATUS_DETAILS_TEST_ID, WORKFLOW_STATUS_TITLE_TEST_ID } from './test_ids'; +import { useBulkGetUserProfiles } from '../../../../common/components/user_profiles/use_bulk_get_user_profiles'; +import { PreferenceFormattedDate } from '../../../../common/components/formatted_date'; +import { useDocumentDetailsContext } from '../../shared/context'; +import { getField } from '../../shared/utils'; + +/** + * Displays info about who last updated the alert's workflow status and when. + */ +export const AlertStatus: FC = () => { + const { getFieldsData } = useDocumentDetailsContext(); + const statusUpdatedBy = getFieldsData('kibana.alert.workflow_user'); + const statusUpdatedAt = getField(getFieldsData('kibana.alert.workflow_status_updated_at')); + + const result = useBulkGetUserProfiles({ uids: new Set(statusUpdatedBy) }); + const user = result.data?.[0]?.user; + + if (!statusUpdatedBy || !statusUpdatedAt || result.isLoading || user == null) { + return null; + } + + return ( + + + + +
+ +
+
+
+ + , + }} + /> + +
+ ); +}; + +AlertStatus.displayName = 'AlertStatus'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts index 78afeb19e0b80..c73c41062f4f8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts @@ -75,6 +75,10 @@ export const MITRE_ATTACK_DETAILS_TEST_ID = `${MITRE_ATTACK_TEST_ID}Details` as export const EVENT_RENDERER_TEST_ID = `${PREFIX}EventRenderer` as const; +export const WORKFLOW_STATUS_TEST_ID = `${PREFIX}WorkflowStatus` as const; +export const WORKFLOW_STATUS_TITLE_TEST_ID = `${WORKFLOW_STATUS_TEST_ID}Title` as const; +export const WORKFLOW_STATUS_DETAILS_TEST_ID = `${WORKFLOW_STATUS_TEST_ID}Details` as const; + /* Investigation section */ export const INVESTIGATION_SECTION_TEST_ID = `${PREFIX}InvestigationSection` as const; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel.cy.ts index 8eac8a410f600..dc2f535be9150 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel.cy.ts @@ -65,6 +65,11 @@ import { ALERTS_URL } from '../../../../urls/navigation'; import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; import { TOASTER } from '../../../../screens/alerts_detection_rules'; import { ELASTICSEARCH_USERNAME, IS_SERVERLESS } from '../../../../env_var_names_constants'; +import { goToAcknowledgedAlerts, goToClosedAlerts } from '../../../../tasks/alerts'; +import { + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_WORKFLOW_STATUS_DETAILS, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_WORKFLOW_STATUS_TITLE, +} from '../../../../screens/expandable_flyout/alert_details_right_panel_overview_tab'; // We need to use the 'soc_manager' role in order to have the 'Respond' action displayed in serverless const isServerless = Cypress.env(IS_SERVERLESS); @@ -171,6 +176,18 @@ describe('Alert details expandable flyout right panel', { tags: ['@ess', '@serve cy.get(TOASTER).should('have.text', 'Successfully marked 1 alert as acknowledged.'); cy.get(EMPTY_ALERT_TABLE).should('exist'); + + goToAcknowledgedAlerts(); + waitForAlertsToPopulate(); + expandAlertAtIndexExpandableFlyout(); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_WORKFLOW_STATUS_TITLE).should( + 'have.text', + 'Last alert status change' + ); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_WORKFLOW_STATUS_DETAILS).should( + 'contain.text', + 'Alert status updated' + ); }); it('should mark as closed', () => { @@ -181,6 +198,18 @@ describe('Alert details expandable flyout right panel', { tags: ['@ess', '@serve cy.get(TOASTER).should('have.text', 'Successfully closed 1 alert.'); cy.get(EMPTY_ALERT_TABLE).should('exist'); + + goToClosedAlerts(); + waitForAlertsToPopulate(); + expandAlertAtIndexExpandableFlyout(); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_WORKFLOW_STATUS_TITLE).should( + 'have.text', + 'Last alert status change' + ); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_WORKFLOW_STATUS_DETAILS).should( + 'contain.text', + 'Alert status updated' + ); }); // these actions are now grouped together as we're not really testing their functionality but just the existence of the option in the dropdown diff --git a/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts b/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts index 2d267bee721dc..3f5eada5ef136 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts @@ -36,6 +36,10 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_TITLE = getDataTe export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_DETAILS = getDataTestSubjectSelector( 'securitySolutionFlyoutMitreAttackDetails' ); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_WORKFLOW_STATUS_TITLE = + getDataTestSubjectSelector('securitySolutionFlyoutWorkflowStatusTitle'); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_WORKFLOW_STATUS_DETAILS = + getDataTestSubjectSelector('securitySolutionFlyoutWorkflowStatusDetails'); /* Investigation section */