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 */