diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.test.tsx
new file mode 100644
index 0000000000000..0f0aee8ed8970
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.test.tsx
@@ -0,0 +1,58 @@
+/*
+ * 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 { render } from '@testing-library/react';
+import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
+import React from 'react';
+import { useGetIntegrationFromRuleId } from '../../../hooks/alert_summary/use_get_integration_from_rule_id';
+import { usePackageIconType } from '@kbn/fleet-plugin/public/hooks';
+import {
+ INTEGRATION_INTEGRATION_ICON_TEST_ID,
+ INTEGRATION_LOADING_SKELETON_TEST_ID,
+ IntegrationIcon,
+} from './integration_icon';
+
+jest.mock('../../../hooks/alert_summary/use_get_integration_from_rule_id');
+jest.mock('@kbn/fleet-plugin/public/hooks');
+
+describe('IntegrationIcon', () => {
+ it('should return a single integration icon', () => {
+ (useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
+ integration: {
+ title: 'title',
+ icons: [{ type: 'type', src: 'src' }],
+ name: 'name',
+ version: 'version',
+ },
+ isLoading: false,
+ });
+ (usePackageIconType as jest.Mock).mockReturnValue('iconType');
+
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId(INTEGRATION_INTEGRATION_ICON_TEST_ID)).toBeInTheDocument();
+ });
+
+ it('should return a single integration loading', () => {
+ (useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
+ integration: {},
+ isLoading: true,
+ });
+
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId(INTEGRATION_LOADING_SKELETON_TEST_ID)).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.tsx
new file mode 100644
index 0000000000000..9646dedae979e
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.tsx
@@ -0,0 +1,55 @@
+/*
+ * 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, { memo } from 'react';
+import { EuiSkeletonText } from '@elastic/eui';
+import { CardIcon } from '@kbn/fleet-plugin/public';
+import type { IconSize } from '@elastic/eui/src/components/icon/icon';
+import { useGetIntegrationFromRuleId } from '../../../hooks/alert_summary/use_get_integration_from_rule_id';
+
+export const INTEGRATION_LOADING_SKELETON_TEST_ID = 'ai-for-soc-alert-integration-loading-skeleton';
+export const INTEGRATION_INTEGRATION_ICON_TEST_ID = 'ai-for-soc-alert-integration-icon';
+
+interface IntegrationProps {
+ /**
+ * Id of the rule the alert was generated by
+ */
+ ruleId: string;
+ /**
+ * Changes the size of the icon. Uses the Eui IconSize interface.
+ * Defaults to s
+ */
+ iconSize?: IconSize;
+}
+
+/**
+ * Renders the icon for the integration that matches the rule id.
+ * In AI for SOC, we can retrieve the integration/package that matches a specific rule, via the related_integrations field on the rule.
+ */
+export const IntegrationIcon = memo(({ ruleId, iconSize = 's' }: IntegrationProps) => {
+ const { integration, isLoading } = useGetIntegrationFromRuleId({ ruleId });
+
+ return (
+
+ {integration ? (
+
+ ) : null}
+
+ );
+});
+IntegrationIcon.displayName = 'IntegrationIcon';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/actions_cell.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/actions_cell.test.tsx
new file mode 100644
index 0000000000000..f7ff7a2612c5f
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/actions_cell.test.tsx
@@ -0,0 +1,58 @@
+/*
+ * 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 { render } from '@testing-library/react';
+import type { Alert } from '@kbn/alerting-types';
+import { ActionsCell, ROW_ACTION_FLYOUT_ICON_TEST_ID } from './actions_cell';
+import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
+import { IOCPanelKey } from '../../../../flyout/ai_for_soc/constants/panel_keys';
+
+jest.mock('@kbn/expandable-flyout');
+
+describe('ActionsCell', () => {
+ it('should render icons', () => {
+ (useExpandableFlyoutApi as jest.Mock).mockReturnValue({
+ openFlyout: jest.fn(),
+ });
+
+ const alert: Alert = {
+ _id: '_id',
+ _index: '_index',
+ };
+
+ const { getByTestId } = render();
+
+ expect(getByTestId(ROW_ACTION_FLYOUT_ICON_TEST_ID)).toBeInTheDocument();
+ });
+
+ it('should open flyout after click', () => {
+ const openFlyout = jest.fn();
+ (useExpandableFlyoutApi as jest.Mock).mockReturnValue({
+ openFlyout,
+ });
+
+ const alert: Alert = {
+ _id: '_id',
+ _index: '_index',
+ };
+
+ const { getByTestId } = render();
+
+ getByTestId(ROW_ACTION_FLYOUT_ICON_TEST_ID).click();
+
+ expect(openFlyout).toHaveBeenCalledWith({
+ right: {
+ id: IOCPanelKey,
+ params: {
+ id: alert._id,
+ indexName: alert._index,
+ },
+ },
+ });
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/actions_cell.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/actions_cell.tsx
new file mode 100644
index 0000000000000..61cf6a8cd1edd
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/actions_cell.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, { memo, useCallback } from 'react';
+import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
+import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import type { Alert } from '@kbn/alerting-types';
+import { i18n } from '@kbn/i18n';
+import { IOCPanelKey } from '../../../../flyout/ai_for_soc/constants/panel_keys';
+
+export const ROW_ACTION_FLYOUT_ICON_TEST_ID = 'alert-summary-table-row-action-flyout-icon';
+
+export interface ActionsCellProps {
+ /**
+ * Alert data passed from the renderCellValue callback via the AlertWithLegacyFormats interface
+ */
+ alert: Alert;
+}
+
+/**
+ * Component used in the AI for SOC alert summary table.
+ * It is passed to the renderActionsCell property of the EuiDataGrid.
+ * It renders all the icons in the row action icons:
+ * - open flyout
+ * - assistant (soon)
+ * - more actions (soon)
+ */
+export const ActionsCell = memo(({ alert }: ActionsCellProps) => {
+ const { openFlyout } = useExpandableFlyoutApi();
+ const onOpenFlyout = useCallback(
+ () =>
+ openFlyout({
+ right: {
+ id: IOCPanelKey,
+ params: {
+ id: alert._id,
+ indexName: alert._index,
+ },
+ },
+ }),
+ [alert, openFlyout]
+ );
+
+ return (
+
+
+
+
+
+ );
+});
+
+ActionsCell.displayName = 'ActionsCell';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.test.tsx
index 1bb861b2803e9..1507dbfd17dc7 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.test.tsx
@@ -5,56 +5,12 @@
* 2.0.
*/
-import {
- getIntegrationComponent,
- groupStatsRenderer,
- Integration,
- INTEGRATION_ICON_TEST_ID,
- INTEGRATION_LOADING_TEST_ID,
-} from './group_stats_renderers';
-import type { GenericBuckets } from '@kbn/grouping/src';
-import { render } from '@testing-library/react';
-import React from 'react';
+import { getIntegrationComponent, groupStatsRenderer } from './group_stats_renderers';
import { useGetIntegrationFromRuleId } from '../../../hooks/alert_summary/use_get_integration_from_rule_id';
-import { usePackageIconType } from '@kbn/fleet-plugin/public/hooks';
jest.mock('../../../hooks/alert_summary/use_get_integration_from_rule_id');
jest.mock('@kbn/fleet-plugin/public/hooks');
-describe('Integration', () => {
- it('should return a single integration icon', () => {
- (useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
- integration: {
- title: 'title',
- icons: [{ type: 'type', src: 'src' }],
- name: 'name',
- version: 'version',
- },
- isLoading: false,
- });
- (usePackageIconType as jest.Mock).mockReturnValue('iconType');
-
- const bucket: GenericBuckets = { key: 'crowdstrike', doc_count: 10 };
-
- const { getByTestId } = render();
-
- expect(getByTestId(INTEGRATION_ICON_TEST_ID)).toBeInTheDocument();
- });
-
- it('should return a single integration loading', () => {
- (useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
- integration: {},
- isLoading: true,
- });
-
- const bucket: GenericBuckets = { key: 'crowdstrike', doc_count: 10 };
-
- const { getByTestId } = render();
-
- expect(getByTestId(INTEGRATION_LOADING_TEST_ID)).toBeInTheDocument();
- });
-});
-
describe('getIntegrationComponent', () => {
it('should return an empty array', () => {
const groupStatsItems = getIntegrationComponent({
@@ -80,13 +36,8 @@ describe('getIntegrationComponent', () => {
expect(groupStatsItems.length).toBe(1);
expect(groupStatsItems[0].component).toMatchInlineSnapshot(`
-
`);
});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.tsx
index 8de66291cb89c..5860da4058356 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.tsx
@@ -5,13 +5,10 @@
* 2.0.
*/
-import { EuiSkeletonText } from '@elastic/eui';
import type { GroupStatsItem, RawBucket } from '@kbn/grouping';
-import React, { memo } from 'react';
+import React from 'react';
import { i18n } from '@kbn/i18n';
-import type { GenericBuckets } from '@kbn/grouping/src';
-import { CardIcon } from '@kbn/fleet-plugin/public';
-import { useGetIntegrationFromRuleId } from '../../../hooks/alert_summary/use_get_integration_from_rule_id';
+import { IntegrationIcon } from '../common/integration_icon';
import { getRulesBadge, getSeverityComponent } from '../../alerts_table/grouping_settings';
import { DEFAULT_GROUP_STATS_RENDERER } from '../../alerts_table/alerts_grouping';
import type { AlertsGroupingAggregation } from '../../alerts_table/grouping_settings/types';
@@ -29,41 +26,6 @@ const STATS_GROUP_SIGNAL_RULE_ID_MULTI = i18n.translate(
}
);
-export const INTEGRATION_ICON_TEST_ID = 'alert-summary-table-integration-cell-renderer-icon';
-export const INTEGRATION_LOADING_TEST_ID = 'alert-summary-table-integration-cell-renderer-loading';
-
-interface IntegrationProps {
- /**
- * Aggregation buckets for integrations
- */
- signalRuleIdBucket: GenericBuckets;
-}
-
-/**
- * Renders the icon for the integration that matches the rule id.
- * In AI for SOC, we can retrieve the integration/package that matches a specific rule, via the related_integrations field on the rule.
- */
-export const Integration = memo(({ signalRuleIdBucket }: IntegrationProps) => {
- const signalRuleId = signalRuleIdBucket.key;
- const { integration, isLoading } = useGetIntegrationFromRuleId({ ruleId: signalRuleId });
-
- return (
-
- {integration ? (
-
- ) : null}
-
- );
-});
-Integration.displayName = 'Integration';
-
/**
* Return a renderer for integration aggregation.
*/
@@ -77,10 +39,13 @@ export const getIntegrationComponent = (
}
if (signalRuleIds.length === 1) {
+ const ruleId = Array.isArray(signalRuleIds[0].key)
+ ? signalRuleIds[0].key[0]
+ : signalRuleIds[0].key;
return [
{
title: STATS_GROUP_SIGNAL_RULE_ID,
- component: ,
+ component: ,
},
];
}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx
index 6c4d1e33cc9d6..0efdd73bed0e2 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx
@@ -20,6 +20,7 @@ import type {
EuiDataGridStyle,
EuiDataGridToolBarVisibilityOptions,
} from '@elastic/eui';
+import { ActionsCell } from './actions_cell';
import { AdditionalToolbarControls } from './additional_toolbar_controls';
import { getDataViewStateFromIndexFields } from '../../../../common/containers/source/use_data_view';
import { inputsSelectors } from '../../../../common/store';
@@ -71,6 +72,7 @@ const columns: EuiDataGridProps['columns'] = [
},
];
+const ACTION_COLUMN_WIDTH = 64; // px
const ALERT_TABLE_CONSUMERS: AlertsTableProps['consumers'] = [AlertConsumers.SIEM];
const RULE_TYPE_IDS = [ESQL_RULE_TYPE_ID, QUERY_RULE_TYPE_ID];
const ROW_HEIGHTS_OPTIONS = { defaultHeight: 40 };
@@ -177,12 +179,14 @@ export const Table = memo(({ dataView, groupingFilters }: TableProps) => {
return (
+ render(
+
+
+
+
+
+ );
+
+describe('', () => {
+ beforeEach(() => {
+ jest.mocked(useDateFormat).mockImplementation(() => dateFormat);
+ jest.mocked(useTimeZone).mockImplementation(() => 'UTC');
+ });
+
+ it('should render component', () => {
+ const { getByTestId } = renderHeader(mockContextValue);
+
+ expect(getByTestId(`${HEADER_TITLE_TEST_ID}Text`)).toHaveTextContent('rule-name');
+ expect(getByTestId(HEADER_SUMMARY_TEST_ID)).toBeInTheDocument();
+
+ expect(getByTestId(HEADER_SEVERITY_TITLE_TEST_ID)).toHaveTextContent('Severity');
+ expect(getByTestId(SEVERITY_VALUE_TEST_ID)).toBeInTheDocument();
+
+ expect(getByTestId(HEADER_RISK_SCORE_TITLE_TEST_ID)).toHaveTextContent('Risk score');
+ expect(getByTestId(RISK_SCORE_VALUE_TEST_ID)).toBeInTheDocument();
+
+ expect(getByTestId(HEADER_INTEGRATION_TITLE_TEST_ID)).toHaveTextContent('Integration');
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/header_title.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/header_title.tsx
new file mode 100644
index 0000000000000..e5d66bf82d6d9
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/header_title.tsx
@@ -0,0 +1,95 @@
+/*
+ * 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, { memo, useMemo } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { IntegrationIcon } from '../../../detections/components/alert_summary/common/integration_icon';
+import { DocumentSeverity } from '../../document_details/right/components/severity';
+import { useBasicDataFromDetailsData } from '../../document_details/shared/hooks/use_basic_data_from_details_data';
+import { FlyoutTitle } from '../../shared/components/flyout_title';
+import { PreferenceFormattedDate } from '../../../common/components/formatted_date';
+import { getAlertTitle } from '../../document_details/shared/utils';
+import { RiskScore } from '../../document_details/right/components/risk_score';
+import { useAIForSOCDetailsContext } from '../context';
+import { AlertHeaderBlock } from '../../shared/components/alert_header_block';
+
+export const HEADER_TITLE_TEST_ID = 'ai-for-soc-alert-flyout-header-title';
+export const HEADER_SUMMARY_TEST_ID = 'ai-for-soc-alert-flyout-header-summary';
+export const HEADER_SEVERITY_TITLE_TEST_ID = 'ai-for-soc-alert-flyout-header-severity';
+export const HEADER_RISK_SCORE_TITLE_TEST_ID = 'ai-for-soc-alert-flyout-header-risk-score';
+export const HEADER_INTEGRATION_TITLE_TEST_ID = 'ai-for-soc-alert-flyout-header-integration';
+
+/**
+ * Header data for the AI for SOC for the alert summary flyout
+ */
+export const HeaderTitle = memo(() => {
+ const { dataFormattedForFieldBrowser, getFieldsData } = useAIForSOCDetailsContext();
+ const { ruleId, ruleName, timestamp } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
+ const title = useMemo(() => getAlertTitle({ ruleName }), [ruleName]);
+
+ const date = useMemo(() => new Date(timestamp), [timestamp]);
+
+ return (
+ <>
+ {timestamp && }
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+ >
+ );
+});
+
+HeaderTitle.displayName = 'HeaderTitle';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/constants/panel_keys.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/constants/panel_keys.ts
new file mode 100644
index 0000000000000..532cd1187481d
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/constants/panel_keys.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export const IOCPanelKey = 'ai-for-soc-details' as const;
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/context.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/context.tsx
new file mode 100644
index 0000000000000..c0765785aec0f
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/context.tsx
@@ -0,0 +1,106 @@
+/*
+ * 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, { createContext, memo, useContext, useMemo } from 'react';
+import type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
+import type { SearchHit } from '../../../common/search_strategy';
+import type { GetFieldsData } from '../document_details/shared/hooks/use_get_fields_data';
+import { FlyoutLoading } from '../shared/components/flyout_loading';
+import { useEventDetails } from '../document_details/shared/hooks/use_event_details';
+import type { AIForSOCDetailsProps } from './types';
+import { FlyoutError } from '../shared/components/flyout_error';
+
+export interface AIForSOCDetailsContext {
+ /**
+ * Id of the document
+ */
+ eventId: string;
+ /**
+ * Name of the index used in the parent's page
+ */
+ indexName: string;
+ /**
+ * An array of field objects with category and value
+ */
+ dataFormattedForFieldBrowser: TimelineEventsDetailsItem[];
+ /**
+ * An object containing fields by type
+ */
+ browserFields: BrowserFields;
+ /**
+ * Retrieves searchHit values for the provided field
+ */
+ getFieldsData: GetFieldsData;
+ /**
+ * The actual raw document object
+ */
+ searchHit: SearchHit;
+}
+
+/**
+ * A context provider for the AI for SOC alert summary flyout
+ */
+export const AIForSOCDetailsContext = createContext(undefined);
+
+export type AIForSOCDetailsProviderProps = {
+ /**
+ * React components to render
+ */
+ children: React.ReactNode;
+} & Partial;
+
+export const AIForSOCDetailsProvider = memo(
+ ({ id, indexName, children }: AIForSOCDetailsProviderProps) => {
+ const { browserFields, dataFormattedForFieldBrowser, getFieldsData, loading, searchHit } =
+ useEventDetails({
+ eventId: id,
+ indexName,
+ });
+ const contextValue = useMemo(
+ () =>
+ dataFormattedForFieldBrowser && id && indexName && searchHit
+ ? {
+ browserFields,
+ dataFormattedForFieldBrowser,
+ eventId: id,
+ getFieldsData,
+ indexName,
+ searchHit,
+ }
+ : undefined,
+ [browserFields, dataFormattedForFieldBrowser, getFieldsData, id, indexName, searchHit]
+ );
+
+ if (loading) {
+ return ;
+ }
+
+ if (!contextValue) {
+ return ;
+ }
+
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+AIForSOCDetailsProvider.displayName = 'AIForSOCDetailsProvider';
+
+export const useAIForSOCDetailsContext = (): AIForSOCDetailsContext => {
+ const contextValue = useContext(AIForSOCDetailsContext);
+
+ if (!contextValue) {
+ throw new Error(
+ 'AIForSOCDetailsContext can only be used within AIForSOCDetailsContext provider'
+ );
+ }
+
+ return contextValue;
+};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/footer.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/footer.tsx
new file mode 100644
index 0000000000000..fc2d60fe07bf4
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/footer.tsx
@@ -0,0 +1,26 @@
+/*
+ * 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 { EuiFlexGroup, EuiFlexItem, EuiFlyoutFooter, EuiPanel } from '@elastic/eui';
+
+export const FLYOUT_FOOTER_TEST_ID = 'ai-for-soc-alert-flyout-footer';
+
+/**
+ * Bottom section of the flyout that contains the take action button
+ */
+export const PanelFooter = () => (
+
+
+
+
+
+
+
+);
+
+PanelFooter.displayName = 'PanelFooter';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/index.tsx
new file mode 100644
index 0000000000000..f79b38d033a81
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/index.tsx
@@ -0,0 +1,38 @@
+/*
+ * 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, { memo } from 'react';
+import { useAIForSOCDetailsContext } from './context';
+import { FlyoutBody } from '../shared/components/flyout_body';
+import { FlyoutNavigation } from '../shared/components/flyout_navigation';
+import type { AIForSOCDetailsProps } from './types';
+import { PanelFooter } from './footer';
+import { FlyoutHeader } from '../shared/components/flyout_header';
+import { HeaderTitle } from './components/header_title';
+
+export const FLYOUT_BODY_TEST_ID = 'ai-for-soc-alert-flyout-body';
+
+/**
+ * Panel to be displayed in AI for SOC alert summary flyout
+ */
+export const AIForSOCPanel: React.FC> = memo(() => {
+ const { eventId } = useAIForSOCDetailsContext();
+
+ return (
+ <>
+
+
+
+
+
+ <>{eventId}>
+
+
+ >
+ );
+});
+AIForSOCPanel.displayName = 'AIForSOCPanel';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/types.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/types.ts
new file mode 100644
index 0000000000000..31da3cb69aee0
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/types.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 type { FlyoutPanelProps } from '@kbn/expandable-flyout';
+import type { IOCPanelKey } from './constants/panel_keys';
+
+export interface AIForSOCDetailsProps extends FlyoutPanelProps {
+ key: typeof IOCPanelKey;
+ params?: {
+ id: string;
+ indexName: string;
+ };
+}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.test.tsx
index b2d8e64c34b45..b5764d7d0f7d1 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.test.tsx
@@ -9,21 +9,24 @@ import React from 'react';
import { render } from '@testing-library/react';
import { DocumentDetailsContext } from '../../shared/context';
import {
- RISK_SCORE_VALUE_TEST_ID,
- SEVERITY_VALUE_TEST_ID,
- FLYOUT_ALERT_HEADER_TITLE_TEST_ID,
- STATUS_BUTTON_TEST_ID,
ALERT_SUMMARY_PANEL_TEST_ID,
- ASSIGNEES_TEST_ID,
ASSIGNEES_EMPTY_TEST_ID,
+ ASSIGNEES_TEST_ID,
+ ASSIGNEES_TITLE_TEST_ID,
+ FLYOUT_ALERT_HEADER_TITLE_TEST_ID,
NOTES_TITLE_TEST_ID,
+ RISK_SCORE_TITLE_TEST_ID,
+ RISK_SCORE_VALUE_TEST_ID,
+ SEVERITY_VALUE_TEST_ID,
+ STATUS_BUTTON_TEST_ID,
+ STATUS_TITLE_TEST_ID,
} from './test_ids';
import { AlertHeaderTitle } from './alert_header_title';
import moment from 'moment-timezone';
import { useDateFormat, useTimeZone } from '../../../../common/lib/kibana';
import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data';
import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser';
-import { TestProvidersComponent } from '../../../../common/mock';
+import { TestProviders } from '../../../../common/mock';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
jest.mock('../../../../common/lib/kibana');
@@ -41,11 +44,11 @@ const HEADER_TEXT_TEST_ID = `${FLYOUT_ALERT_HEADER_TITLE_TEST_ID}Text`;
const renderHeader = (contextValue: DocumentDetailsContext) =>
render(
-
+
-
+
);
describe('', () => {
@@ -55,12 +58,19 @@ describe('', () => {
});
it('should render component', () => {
+ (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true);
+
const { getByTestId, queryByTestId } = renderHeader(mockContextValue);
expect(getByTestId(HEADER_TEXT_TEST_ID)).toHaveTextContent('rule-name');
expect(getByTestId(SEVERITY_VALUE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(ALERT_SUMMARY_PANEL_TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(STATUS_TITLE_TEST_ID)).toHaveTextContent('Status');
+ expect(getByTestId(RISK_SCORE_TITLE_TEST_ID)).toHaveTextContent('Risk score');
+ expect(getByTestId(ASSIGNEES_TITLE_TEST_ID)).toHaveTextContent('Assignees');
+ expect(queryByTestId(NOTES_TITLE_TEST_ID)).not.toBeInTheDocument();
+
expect(getByTestId(RISK_SCORE_VALUE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(STATUS_BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(ASSIGNEES_TEST_ID)).toBeInTheDocument();
@@ -81,6 +91,6 @@ describe('', () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false);
const { getByTestId } = renderHeader(mockContextValue);
- expect(getByTestId(NOTES_TITLE_TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(NOTES_TITLE_TEST_ID)).toHaveTextContent('Notes');
});
});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx
index 529e3d43b6056..2308e3338df74 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx
@@ -6,8 +6,9 @@
*/
import React, { memo, useCallback, useMemo } from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiLink } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer } from '@elastic/eui';
import { ALERT_WORKFLOW_ASSIGNEE_IDS } from '@kbn/rule-data-utils';
+import { FormattedMessage } from '@kbn/i18n-react';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { Notes } from './notes';
import { useRuleDetailsLink } from '../../shared/hooks/use_rule_details_link';
@@ -18,10 +19,16 @@ import { useRefetchByScope } from '../hooks/use_refetch_by_scope';
import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data';
import { useDocumentDetailsContext } from '../../shared/context';
import { PreferenceFormattedDate } from '../../../../common/components/formatted_date';
-import { FLYOUT_ALERT_HEADER_TITLE_TEST_ID, ALERT_SUMMARY_PANEL_TEST_ID } from './test_ids';
+import {
+ ALERT_SUMMARY_PANEL_TEST_ID,
+ ASSIGNEES_TITLE_TEST_ID,
+ FLYOUT_ALERT_HEADER_TITLE_TEST_ID,
+ RISK_SCORE_TITLE_TEST_ID,
+} from './test_ids';
import { Assignees } from './assignees';
import { FlyoutTitle } from '../../../shared/components/flyout_title';
import { getAlertTitle } from '../../shared/utils';
+import { AlertHeaderBlock } from '../../../shared/components/alert_header_block';
// minWidth for each block, allows to switch for a 1 row 4 blocks to 2 rows with 2 block each
const blockStyles = {
@@ -78,9 +85,50 @@ export const AlertHeaderTitle = memo(() => {
refetchFlyoutData();
}, [refetch, refetchFlyoutData]);
+ const riskScore = useMemo(
+ () => (
+
+ }
+ data-test-subj={RISK_SCORE_TITLE_TEST_ID}
+ >
+
+
+ ),
+ [getFieldsData]
+ );
+
+ const assignees = useMemo(
+ () => (
+
+ }
+ data-test-subj={ASSIGNEES_TITLE_TEST_ID}
+ >
+
+
+ ),
+ [alertAssignees, eventId, isPreview, onAssigneesUpdated]
+ );
+
return (
<>
-
+
{timestamp && }
@@ -97,17 +145,8 @@ export const AlertHeaderTitle = memo(() => {
-
-
-
-
-
-
+ {riskScore}
+ {assignees}
) : (
{
wrap
data-test-subj={ALERT_SUMMARY_PANEL_TEST_ID}
>
-
+
-
-
-
+ {riskScore}
-
+
-
-
-
+ {assignees}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/assignees.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/assignees.test.tsx
index 32e27b36e25ac..88ab099256a79 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/assignees.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/assignees.test.tsx
@@ -6,13 +6,9 @@
*/
import React from 'react';
-import { render, fireEvent } from '@testing-library/react';
+import { fireEvent, render } from '@testing-library/react';
-import {
- ASSIGNEES_ADD_BUTTON_TEST_ID,
- ASSIGNEES_EMPTY_TEST_ID,
- ASSIGNEES_TITLE_TEST_ID,
-} from './test_ids';
+import { ASSIGNEES_ADD_BUTTON_TEST_ID, ASSIGNEES_EMPTY_TEST_ID } from './test_ids';
import { Assignees } from './assignees';
import { useGetCurrentUserProfile } from '../../../../common/components/user_profiles/use_get_current_user_profile';
@@ -25,9 +21,9 @@ import { ASSIGNEES_APPLY_BUTTON_TEST_ID } from '../../../../common/components/as
import { useLicense } from '../../../../common/hooks/use_license';
import { useUpsellingMessage } from '../../../../common/hooks/use_upselling';
import {
+ USER_AVATAR_ITEM_TEST_ID,
USERS_AVATARS_COUNT_BADGE_TEST_ID,
USERS_AVATARS_PANEL_TEST_ID,
- USER_AVATAR_ITEM_TEST_ID,
} from '../../../../common/components/user_profiles/test_ids';
import { useAlertsPrivileges } from '../../../../detections/containers/detection_engine/alerts/use_alerts_privileges';
@@ -92,7 +88,6 @@ describe('', () => {
it('should render component', () => {
const { getByTestId } = renderAssignees();
- expect(getByTestId(ASSIGNEES_TITLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(USERS_AVATARS_PANEL_TEST_ID)).toBeInTheDocument();
expect(getByTestId(ASSIGNEES_ADD_BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(ASSIGNEES_ADD_BUTTON_TEST_ID)).not.toBeDisabled();
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/assignees.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/assignees.tsx
index aa8360c30d292..e17a0a5b34003 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/assignees.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/assignees.tsx
@@ -18,8 +18,6 @@ import {
useGeneratedHtmlId,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n-react';
-import { AlertHeaderBlock } from './alert_header_block';
import { useSetAlertAssignees } from '../../../../common/components/toolbar/bulk_actions/use_set_alert_assignees';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import { ASSIGNEES_PANEL_WIDTH } from '../../../../common/components/assignees/constants';
@@ -34,7 +32,6 @@ import {
ASSIGNEES_ADD_BUTTON_TEST_ID,
ASSIGNEES_EMPTY_TEST_ID,
ASSIGNEES_TEST_ID,
- ASSIGNEES_TITLE_TEST_ID,
} from './test_ids';
const UpdateAssigneesButton: FC<{
@@ -79,8 +76,8 @@ export interface AssigneesProps {
/**
* Document assignees details displayed in flyout right section header
*/
-export const Assignees: FC = memo(
- ({ eventId, assignedUserIds, onAssigneesUpdated, isPreview }) => {
+export const Assignees = memo(
+ ({ eventId, assignedUserIds, onAssigneesUpdated, isPreview }: AssigneesProps) => {
const isPlatinumPlus = useLicense().isPlatinumPlus();
const upsellingMessage = useUpsellingMessage('alert_assignments');
@@ -159,15 +156,7 @@ export const Assignees: FC = memo(
]);
return (
-
- }
- data-test-subj={ASSIGNEES_TITLE_TEST_ID}
- >
+ <>
{isPreview ? (
{getEmptyTagValue()}
) : (
@@ -180,7 +169,7 @@ export const Assignees: FC = memo(
{updateAssigneesPopover}
)}
-
+ >
);
}
);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.tsx
index 32e85b786f6d2..04e2bebcd0213 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.tsx
@@ -13,7 +13,7 @@ import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_f
import { useDocumentDetailsContext } from '../../shared/context';
import { PreferenceFormattedDate } from '../../../../common/components/formatted_date';
import { FLYOUT_EVENT_HEADER_TITLE_TEST_ID } from './test_ids';
-import { getField, getEventTitle } from '../../shared/utils';
+import { getEventTitle, getField } from '../../shared/utils';
/**
* Event details flyout right section header
@@ -32,7 +32,7 @@ export const EventHeaderTitle = memo(() => {
return (
<>
-
+
{timestamp && }
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/notes.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/notes.tsx
index 6538854b93fe6..9e9bc064b6191 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/notes.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/notes.tsx
@@ -40,7 +40,7 @@ import {
selectNotesByDocumentId,
} from '../../../../notes/store/notes.slice';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
-import { AlertHeaderBlock } from './alert_header_block';
+import { AlertHeaderBlock } from '../../../shared/components/alert_header_block';
import { LeftPanelNotesTab } from '../../left';
import { useNavigateToLeftPanel } from '../../shared/hooks/use_navigate_to_left_panel';
@@ -154,6 +154,7 @@ export const Notes = memo(() => {
return (
- );
+ return {alertRiskScore};
});
RiskScore.displayName = 'RiskScore';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/severity.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/severity.test.tsx
index 5402f6f229671..b34da68d3b400 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/severity.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/severity.test.tsx
@@ -7,29 +7,20 @@
import React from 'react';
import { render } from '@testing-library/react';
-import { DocumentDetailsContext } from '../../shared/context';
import { SEVERITY_VALUE_TEST_ID } from './test_ids';
import { DocumentSeverity } from './severity';
import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data';
import { TestProviders } from '../../../../common/mock';
-const renderDocumentSeverity = (contextValue: DocumentDetailsContext) =>
- render(
-
-
-
-
-
- );
-
describe('', () => {
it('should render severity information', () => {
- const contextValue = {
- getFieldsData: jest.fn().mockImplementation(mockGetFieldsData),
- scopeId: 'scopeId',
- } as unknown as DocumentDetailsContext;
+ const getFieldsData = jest.fn().mockImplementation(mockGetFieldsData);
- const { getByTestId } = renderDocumentSeverity(contextValue);
+ const { getByTestId } = render(
+
+
+
+ );
const severity = getByTestId(SEVERITY_VALUE_TEST_ID);
expect(severity).toBeInTheDocument();
@@ -37,34 +28,37 @@ describe('', () => {
});
it('should render empty component if missing getFieldsData value', () => {
- const contextValue = {
- getFieldsData: jest.fn(),
- scopeId: 'scopeId',
- } as unknown as DocumentDetailsContext;
+ const getFieldsData = jest.fn();
- const { container } = renderDocumentSeverity(contextValue);
+ const { container } = render(
+
+
+
+ );
expect(container).toBeEmptyDOMElement();
});
it('should render empty component if getFieldsData is invalid array', () => {
- const contextValue = {
- getFieldsData: jest.fn().mockImplementation(() => ['abc']),
- scopeId: 'scopeId',
- } as unknown as DocumentDetailsContext;
+ const getFieldsData = jest.fn().mockImplementation(() => ['abc']);
- const { container } = renderDocumentSeverity(contextValue);
+ const { container } = render(
+
+
+
+ );
expect(container).toBeEmptyDOMElement();
});
it('should render empty component if getFieldsData is invalid string', () => {
- const contextValue = {
- getFieldsData: jest.fn().mockImplementation(() => 'abc'),
- scopeId: 'scopeId',
- } as unknown as DocumentDetailsContext;
+ const getFieldsData = jest.fn().mockImplementation(() => 'abc');
- const { container } = renderDocumentSeverity(contextValue);
+ const { container } = render(
+
+
+
+ );
expect(container).toBeEmptyDOMElement();
});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/severity.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/severity.tsx
index 7ae0d243d236f..1014c7ac0f74a 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/severity.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/severity.tsx
@@ -5,41 +5,79 @@
* 2.0.
*/
-import React, { memo } from 'react';
+import React, { memo, useMemo } from 'react';
import { ALERT_SEVERITY } from '@kbn/rule-data-utils';
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
+import { upperFirst } from 'lodash/fp';
+import { EuiBadge, useEuiTheme } from '@elastic/eui';
+import { SEVERITY_VALUE_TEST_ID } from './test_ids';
+import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data';
import { CellActions } from '../../shared/components/cell_actions';
-import { useDocumentDetailsContext } from '../../shared/context';
-import { SeverityBadge } from '../../../../common/components/severity_badge';
+import { useRiskSeverityColors } from '../../../../common/utils/risk_color_palette';
const isSeverity = (x: unknown): x is Severity =>
x === 'low' || x === 'medium' || x === 'high' || x === 'critical';
+export interface DocumentSeverityProps {
+ /**
+ * Retrieves searchHit values for the provided field
+ */
+ getFieldsData: GetFieldsData;
+ /**
+ * If true, show cell actions to allow users to filter, toggle column, copy to clipboard...
+ * Default to false.
+ */
+ showCellActions?: boolean;
+}
+
/**
* Document details severity displayed in flyout right section header
*/
-export const DocumentSeverity = memo(() => {
- const { getFieldsData } = useDocumentDetailsContext();
- const fieldsData = getFieldsData(ALERT_SEVERITY);
+export const DocumentSeverity = memo(
+ ({ getFieldsData, showCellActions = false }: DocumentSeverityProps) => {
+ const { euiTheme } = useEuiTheme();
- if (!fieldsData) {
- return null;
- }
+ const severityToColorMap = useRiskSeverityColors();
- let alertSeverity: Severity;
- if (typeof fieldsData === 'string' && isSeverity(fieldsData)) {
- alertSeverity = fieldsData;
- } else if (Array.isArray(fieldsData) && fieldsData.length > 0 && isSeverity(fieldsData[0])) {
- alertSeverity = fieldsData[0];
- } else {
- return null;
- }
+ const severity: Severity | null = useMemo(() => {
+ const fieldsData = getFieldsData(ALERT_SEVERITY);
+
+ if (typeof fieldsData === 'string' && isSeverity(fieldsData)) {
+ return fieldsData;
+ } else if (Array.isArray(fieldsData) && fieldsData.length > 0 && isSeverity(fieldsData[0])) {
+ return fieldsData[0];
+ } else {
+ return null;
+ }
+ }, [getFieldsData]);
- return (
-
-
-
- );
-});
+ const displayValue = useMemo(() => (severity && upperFirst(severity)) ?? null, [severity]);
+
+ const color = useMemo(
+ () => (severity && severityToColorMap[severity]) ?? euiTheme.colors.textSubdued,
+ [severity, euiTheme.colors.textSubdued, severityToColorMap]
+ );
+
+ return (
+ <>
+ {severity && (
+ <>
+ {showCellActions ? (
+
+
+ {displayValue}
+
+
+ ) : (
+
+ {displayValue}
+
+ )}
+ >
+ )}
+ >
+ );
+ }
+);
DocumentSeverity.displayName = 'DocumentSeverity';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/status.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/status.tsx
index 601201f2e58e8..f338db2495afd 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/status.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/status.tsx
@@ -9,7 +9,7 @@ import type { FC } from 'react';
import React, { useMemo } from 'react';
import { find } from 'lodash/fp';
import { FormattedMessage } from '@kbn/i18n-react';
-import { AlertHeaderBlock } from './alert_header_block';
+import { AlertHeaderBlock } from '../../../shared/components/alert_header_block';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import { SIGNAL_STATUS_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants';
import { StatusPopoverButton } from './status_popover_button';
@@ -52,6 +52,7 @@ export const DocumentStatus: FC = () => {
return (
),
},
+ {
+ key: IOCPanelKey,
+ component: (props) => (
+
+
+
+ ),
+ },
];
export const SECURITY_SOLUTION_ON_CLOSE_EVENT = `expandable-flyout-on-close-${Flyouts.securitySolution}`;
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_header_block.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/alert_header_block.test.tsx
similarity index 100%
rename from x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_header_block.test.tsx
rename to x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/alert_header_block.test.tsx
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_header_block.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/alert_header_block.tsx
similarity index 72%
rename from x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_header_block.tsx
rename to x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/alert_header_block.tsx
index fac083f90de5f..1cce848b71460 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/alert_header_block.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/alert_header_block.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import type { ReactElement, ReactNode, VFC } from 'react';
+import type { ReactElement, ReactNode } from 'react';
import React, { memo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
@@ -18,6 +18,12 @@ export interface AlertHeaderBlockProps {
* React component to render as the value
*/
children: ReactNode;
+ /**
+ * If true, adds a slight 1px border on all edges.
+ * False by default.
+ * This is passed to the EuiPanel's hasBorder property.
+ */
+ hasBorder?: boolean;
/**
* data-test-subj to use for the title
*/
@@ -27,9 +33,14 @@ export interface AlertHeaderBlockProps {
/**
* Reusable component for rendering a block with rounded edges, show a title and value below one another
*/
-export const AlertHeaderBlock: VFC = memo(
- ({ title, children, 'data-test-subj': dataTestSubj }) => (
-
+export const AlertHeaderBlock = memo(
+ ({
+ title,
+ children,
+ hasBorder = false,
+ 'data-test-subj': dataTestSubj,
+ }: AlertHeaderBlockProps) => (
+
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx
index 92ac1b6821781..f629aaf371bec 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx
@@ -13,11 +13,12 @@ import {
EuiFlexGroup,
EuiFlexItem,
type EuiIconProps,
- useEuiTheme,
EuiSkeletonText,
+ useEuiTheme,
} from '@elastic/eui';
import type { FlyoutPanelHistory } from '@kbn/expandable-flyout';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
+import { IOCPanelKey } from '../../ai_for_soc/constants/panel_keys';
import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date';
import { DocumentDetailsRightPanelKey } from '../../document_details/shared/constants/panel_keys';
import { useBasicDataFromDetailsData } from '../../document_details/shared/hooks/use_basic_data_from_details_data';
@@ -29,11 +30,11 @@ import { useRuleDetails } from '../../rule_details/hooks/use_rule_details';
import {
DOCUMENT_DETAILS_HISTORY_ROW_TEST_ID,
GENERIC_HISTORY_ROW_TEST_ID,
+ HISTORY_ROW_LOADING_TEST_ID,
HOST_HISTORY_ROW_TEST_ID,
NETWORK_HISTORY_ROW_TEST_ID,
RULE_HISTORY_ROW_TEST_ID,
USER_HISTORY_ROW_TEST_ID,
- HISTORY_ROW_LOADING_TEST_ID,
} from './test_ids';
import { HostPanelKey, UserPanelKey } from '../../entity_details/shared/constants';
@@ -56,6 +57,7 @@ export interface FlyoutHistoryRowProps {
export const FlyoutHistoryRow: FC = memo(({ item, index }) => {
switch (item.panel.id) {
case DocumentDetailsRightPanelKey:
+ case IOCPanelKey:
return ;
case RulePanelKey:
return ;