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 ( - render( - - - - - - ); - describe('', () => { it('should render risk score information', () => { - const contextValue = { - getFieldsData: jest.fn().mockImplementation(mockGetFieldsData), - } as unknown as DocumentDetailsContext; + const getFieldsData = jest.fn().mockImplementation(mockGetFieldsData); - const { getByTestId } = renderRiskScore(contextValue); + const { getByTestId } = render( + + + + ); - expect(getByTestId(RISK_SCORE_TITLE_TEST_ID)).toBeInTheDocument(); const riskScore = getByTestId(RISK_SCORE_VALUE_TEST_ID); expect(riskScore).toBeInTheDocument(); expect(riskScore).toHaveTextContent('0'); }); it('should render empty component if missing getFieldsData value', () => { - const contextValue = { - getFieldsData: jest.fn(), - } as unknown as DocumentDetailsContext; + const getFieldsData = jest.fn(); - const { container } = renderRiskScore(contextValue); + const { container } = render( + + + + ); expect(container).toBeEmptyDOMElement(); }); it('should render empty component if getFieldsData is invalid', () => { - const contextValue = { - getFieldsData: jest.fn().mockImplementation(() => 123), - } as unknown as DocumentDetailsContext; + const getFieldsData = jest.fn().mockImplementation(() => 123); - const { container } = renderRiskScore(contextValue); + const { container } = render( + + + + ); expect(container).toBeEmptyDOMElement(); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/risk_score.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/risk_score.tsx index fba4343e97499..ee643e36cc3ff 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/risk_score.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/risk_score.tsx @@ -7,16 +7,20 @@ import React, { memo } from 'react'; import { ALERT_RISK_SCORE } from '@kbn/rule-data-utils'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { AlertHeaderBlock } from './alert_header_block'; -import { RISK_SCORE_TITLE_TEST_ID, RISK_SCORE_VALUE_TEST_ID } from './test_ids'; -import { useDocumentDetailsContext } from '../../shared/context'; +import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data'; +import { RISK_SCORE_VALUE_TEST_ID } from './test_ids'; + +export interface RiskScoreProps { + /** + * Retrieves searchHit values for the provided field + */ + getFieldsData: GetFieldsData; +} /** * Document details risk score displayed in flyout right section header */ -export const RiskScore = memo(() => { - const { getFieldsData } = useDocumentDetailsContext(); +export const RiskScore = memo(({ getFieldsData }: RiskScoreProps) => { const fieldsData = getFieldsData(ALERT_RISK_SCORE); if (!fieldsData) { @@ -32,19 +36,7 @@ export const RiskScore = memo(() => { return null; } - return ( - - } - data-test-subj={RISK_SCORE_TITLE_TEST_ID} - > - {alertRiskScore} - - ); + 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 ;