Skip to content

Commit

Permalink
[Security Solution][Alert details] - bring back last alert status cha…
Browse files Browse the repository at this point in the history
…nge to flyout
  • Loading branch information
PhilippeOberti committed Dec 27, 2024
1 parent c382c20 commit 1a99cc5
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -106,6 +107,7 @@ describe('<AboutSection />', () => {
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();

Expand Down Expand Up @@ -135,6 +137,7 @@ describe('<AboutSection />', () => {
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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -42,6 +43,7 @@ export const AboutSection = memo(() => {
<AlertDescription />
<Reason />
<MitreAttack />
<AlertStatus />
</>
) : (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
<TestProviders>
<DocumentDetailsContext.Provider value={contextValue}>
<AlertStatus />
</DocumentDetailsContext.Provider>
</TestProviders>
);

const mockUserProfiles = [
{ uid: 'user-id-1', enabled: true, user: { username: 'user1', full_name: 'User 1' }, data: {} },
];

describe('<AlertStatus />', () => {
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();
});
});
});
Original file line number Diff line number Diff line change
@@ -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 (
<EuiFlexGroup direction="column" gutterSize="s">
<EuiSpacer size="xs" />
<EuiFlexItem data-test-subj={WORKFLOW_STATUS_TITLE_TEST_ID}>
<EuiTitle size="xxs">
<h5>
<FormattedMessage
id="xpack.securitySolution.flyout.right.about.status.statusHistoryTitle"
defaultMessage="Last alert status change"
/>
</h5>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem data-test-subj={WORKFLOW_STATUS_DETAILS_TEST_ID}>
<FormattedMessage
id="xpack.securitySolution.flyout.right.about.status.statusHistoryDetails"
defaultMessage="Alert status updated by {user} on {date}"
values={{
user: getUserDisplayName(user),
date: <PreferenceFormattedDate value={new Date(statusUpdatedAt)} />,
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

AlertStatus.displayName = 'AlertStatus';
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand Down

0 comments on commit 1a99cc5

Please sign in to comment.