From 08c614f2aba03cb0d6fdf9a46589cf9997487fe2 Mon Sep 17 00:00:00 2001 From: PhilippeOberti Date: Thu, 10 Apr 2025 16:47:43 -0500 Subject: [PATCH 1/2] [AI4DSOC] Alert summary table custom cell renderers --- .../table/basic_cell_renderer.test.tsx | 135 ++++++++++++++++++ .../table/basic_cell_renderer.tsx | 37 +++++ ...elated_integrations_cell_renderer.test.tsx | 96 +++++++++++++ ...ert_related_integrations_cell_renderer.tsx | 86 +++++++++++ ...bana_alert_severity_cell_renderer.test.tsx | 101 +++++++++++++ .../kibana_alert_severity_cell_renderer.tsx | 62 ++++++++ .../alert_summary/table/render_cell.test.tsx | 44 ++++++ .../alert_summary/table/render_cell.tsx | 52 +++---- .../components/alert_summary/table/table.tsx | 19 +-- .../use_get_integration_from_package_name.ts | 51 +++++++ ...ert_field_value_as_string_or_null.test.tsx | 102 +++++++++++++ ...get_alert_field_value_as_string_or_null.ts | 41 ++++++ 12 files changed, 786 insertions(+), 40 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_severity_cell_renderer.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_severity_cell_renderer.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_package_name.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/utils/get_alert_field_value_as_string_or_null.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detections/utils/get_alert_field_value_as_string_or_null.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.test.tsx new file mode 100644 index 0000000000000..f48b8891e3a50 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.test.tsx @@ -0,0 +1,135 @@ +/* + * 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 { BasicCellRenderer } from './basic_cell_renderer'; +import { TestProviders } from '../../../../common/mock'; +import { getEmptyValue } from '../../../../common/components/empty_value'; +import { CellValue } from './render_cell'; + +describe('BasicCellRenderer', () => { + it('should handle missing field', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: 'value1', + }; + const field = 'field'; + + const { getByText } = render( + + + + ); + + expect(getByText(getEmptyValue())).toBeInTheDocument(); + }); + + it('should handle string value', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: 'value1', + }; + const field = 'field1'; + + const { getByText } = render( + + + + ); + + expect(getByText('value1')).toBeInTheDocument(); + }); + + it('should handle number value', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: 123, + }; + const columnId = 'field1'; + + const { getByText } = render( + + + + ); + + expect(getByText('123')).toBeInTheDocument(); + }); + + it('should handle array of booleans', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: [true, false], + }; + const field = 'field1'; + + const { getByText } = render( + + + + ); + + expect(getByText('true, false')).toBeInTheDocument(); + }); + + it('should handle array of numbers', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: [1, 2], + }; + const field = 'field1'; + + const { getByText } = render( + + + + ); + + expect(getByText('1, 2')).toBeInTheDocument(); + }); + + it('should handle array of null', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: [null, null], + }; + const field = 'field1'; + + const { getByText } = render( + + + + ); + + expect(getByText(',')).toBeInTheDocument(); + }); + + it('should join array of JsonObjects', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: [{ subField1: 'value1', subField2: 'value2' }], + }; + const field = 'field1'; + + const { getByText } = render( + + + + ); + + expect(getByText('[object Object]')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.tsx new file mode 100644 index 0000000000000..2d2d229ca4954 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.tsx @@ -0,0 +1,37 @@ +/* + * 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 type { Alert } from '@kbn/alerting-types'; +import { getOrEmptyTagFromValue } from '../../../../common/components/empty_value'; +import { getAlertFieldValueAsStringOrNull } from '../../../utils/get_alert_field_value_as_string_or_null'; + +export interface BasicCellRendererProps { + /** + * Alert data passed from the renderCellValue callback via the AlertWithLegacyFormats interface + */ + alert: Alert; + /** + * Column id passed from the renderCellValue callback via EuiDataGridProps['renderCellValue'] interface + */ + field: string; +} + +/** + * Renders all the basic table cell values. + * Component used in the AI for SOC alert summary table. + */ +export const BasicCellRenderer = memo(({ alert, field }: BasicCellRendererProps) => { + const displayValue: string | null = useMemo( + () => getAlertFieldValueAsStringOrNull(alert, field), + [alert, field] + ); + + return <>{getOrEmptyTagFromValue(displayValue)}; +}); + +BasicCellRenderer.displayName = 'BasicCellRenderer'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.test.tsx new file mode 100644 index 0000000000000..15db07b321448 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.test.tsx @@ -0,0 +1,96 @@ +/* + * 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 { + ICON_TEST_ID, + KibanaAlertRelatedIntegrationsCellRenderer, + SKELETON_TEST_ID, +} from './kibana_alert_related_integrations_cell_renderer'; +import { useGetIntegrationFromPackageName } from '../../../hooks/alert_summary/use_get_integration_from_package_name'; +import { ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils'; + +jest.mock('../../../hooks/alert_summary/use_get_integration_from_package_name'); + +describe('KibanaAlertRelatedIntegrationsCellRenderer', () => { + it('should handle missing field', () => { + (useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({ + integration: null, + isLoading: false, + }); + + const alert: Alert = { + _id: '_id', + _index: '_index', + }; + + const { queryByTestId } = render(); + + expect(queryByTestId(SKELETON_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(ICON_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should handle not finding matching integration', () => { + (useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({ + integration: null, + isLoading: false, + }); + + const alert: Alert = { + _id: '_id', + _index: '_index', + [ALERT_RULE_PARAMETERS]: ['splunk'], + }; + + const { queryByTestId } = render(); + + expect(queryByTestId(SKELETON_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(ICON_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should show loading', () => { + (useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({ + integration: null, + isLoading: true, + }); + + const alert: Alert = { + _id: '_id', + _index: '_index', + [ALERT_RULE_PARAMETERS]: ['splunk'], + }; + + const { getByTestId, queryByTestId } = render( + + ); + + expect(getByTestId(SKELETON_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(ICON_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should show integration icon', () => { + (useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({ + integration: { name: 'Splunk', icon: ['icon'] }, + isLoading: false, + }); + + const alert: Alert = { + _id: '_id', + _index: '_index', + [ALERT_RULE_PARAMETERS]: ['splunk'], + }; + + const { getByTestId, queryByTestId } = render( + + ); + + expect(queryByTestId(SKELETON_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.tsx new file mode 100644 index 0000000000000..9348eae561b42 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.tsx @@ -0,0 +1,86 @@ +/* + * 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 type { JsonValue } from '@kbn/utility-types'; +import { CardIcon } from '@kbn/fleet-plugin/public'; +import { EuiSkeletonText } from '@elastic/eui'; +import type { Alert } from '@kbn/alerting-types'; +import { ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils'; +import { useGetIntegrationFromPackageName } from '../../../hooks/alert_summary/use_get_integration_from_package_name'; + +export const SKELETON_TEST_ID = 'alert-summary-table-related-integrations-cell-renderer-skeleton'; +export const ICON_TEST_ID = 'alert-summary-table-related-integrations-cell-renderer-icon'; + +const RELATED_INTEGRATIONS_FIELD = 'related_integrations'; +const PACKAGE_FIELD = 'package'; + +export interface KibanaAlertRelatedIntegrationsCellRendererProps { + /** + * Alert data passed from the renderCellValue callback via the AlertWithLegacyFormats interface + */ + alert: Alert; +} + +/** + * Renders an integration/package icon. Retrieves the package name from the kibana.alert.rule.parameters field in the alert, + * fetches all integrations/packages and use the icon from the one that matches by name. + * Used in AI for SOC alert summary table. + */ +export const KibanaAlertRelatedIntegrationsCellRenderer = memo( + ({ alert }: KibanaAlertRelatedIntegrationsCellRendererProps) => { + const packageName: string | null = useMemo(() => { + const values: JsonValue[] | undefined = alert[ALERT_RULE_PARAMETERS]; + + if (Array.isArray(values) && values.length === 1) { + const value: JsonValue = values[0]; + if ( + !value || + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' || + Array.isArray(value) + ) + return null; + + const relatedIntegration = value[RELATED_INTEGRATIONS_FIELD]; + if ( + !relatedIntegration || + typeof relatedIntegration === 'string' || + typeof relatedIntegration === 'number' || + typeof relatedIntegration === 'boolean' || + Array.isArray(relatedIntegration) + ) + return 'splunk'; // null; + + return relatedIntegration[PACKAGE_FIELD] as string; + } + + return null; + }, [alert]); + + const { integration, isLoading } = useGetIntegrationFromPackageName({ packageName }); + + return ( + + {integration ? ( + + ) : null} + + ); + } +); + +KibanaAlertRelatedIntegrationsCellRenderer.displayName = + 'KibanaAlertRelatedIntegrationsCellRenderer'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_severity_cell_renderer.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_severity_cell_renderer.test.tsx new file mode 100644 index 0000000000000..f4800556365cd --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_severity_cell_renderer.test.tsx @@ -0,0 +1,101 @@ +/* + * 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 { TestProviders } from '../../../../common/mock'; +import { + BADGE_TEST_ID, + KibanaAlertSeverityCellRenderer, +} from './kibana_alert_severity_cell_renderer'; +import { ALERT_SEVERITY } from '@kbn/rule-data-utils'; + +describe('KibanaAlertSeverityCellRenderer', () => { + it('should handle missing field', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + }; + + const { container } = render( + + + + ); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should show low', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + [ALERT_SEVERITY]: ['low'], + }; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId(BADGE_TEST_ID)).toHaveTextContent('Low'); + expect(getByTestId(BADGE_TEST_ID)).toHaveStyle('--euiBadgeBackgroundColor: #54B399'); + }); + + it('should show medium', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + [ALERT_SEVERITY]: ['medium'], + }; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId(BADGE_TEST_ID)).toHaveTextContent('Medium'); + expect(getByTestId(BADGE_TEST_ID)).toHaveStyle('--euiBadgeBackgroundColor: #D6BF57'); + }); + + it('should show high', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + [ALERT_SEVERITY]: ['high'], + }; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId(BADGE_TEST_ID)).toHaveTextContent('High'); + expect(getByTestId(BADGE_TEST_ID)).toHaveStyle('--euiBadgeBackgroundColor: #DA8B45'); + }); + + it('should show critical', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + [ALERT_SEVERITY]: ['critical'], + }; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId(BADGE_TEST_ID)).toHaveTextContent('Critical'); + expect(getByTestId(BADGE_TEST_ID)).toHaveStyle('--euiBadgeBackgroundColor: #E7664C'); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_severity_cell_renderer.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_severity_cell_renderer.tsx new file mode 100644 index 0000000000000..af80d5d7f4480 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_severity_cell_renderer.tsx @@ -0,0 +1,62 @@ +/* + * 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 { EuiBadge, useEuiTheme } from '@elastic/eui'; +import type { Alert } from '@kbn/alerting-types'; +import { ALERT_SEVERITY } from '@kbn/rule-data-utils'; +import type { JsonValue } from '@kbn/utility-types'; +import { getSeverityColor } from '../../alerts_kpis/severity_level_panel/helpers'; + +export const BADGE_TEST_ID = 'alert-summary-table-severity-cell-renderer'; + +export interface KibanaAlertSeverityCellRendererProps { + /** + * Alert data passed from the renderCellValue callback via the AlertWithLegacyFormats interface + */ + alert: Alert; +} + +/** + * Renders a EuiBadge for the kibana.alert.severity field. + * Used in AI for SOC alert summary table. + */ +export const KibanaAlertSeverityCellRenderer = memo( + ({ alert }: KibanaAlertSeverityCellRendererProps) => { + const { euiTheme } = useEuiTheme(); + + const displayValue: string | null = useMemo(() => { + const values: JsonValue[] | undefined = alert[ALERT_SEVERITY]; + + if (Array.isArray(values) && values.length === 1) { + const value: JsonValue = values[0]; + return value && typeof value === 'string' + ? String(value).charAt(0).toUpperCase() + String(value).slice(1) + : null; + } + + return null; + }, [alert]); + + const color: string = useMemo( + () => getSeverityColor(displayValue || '', euiTheme), + [displayValue, euiTheme] + ); + + return ( + <> + {displayValue && ( + + {displayValue} + + )} + + ); + } +); + +KibanaAlertSeverityCellRenderer.displayName = 'KibanaAlertSeverityCellRenderer'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.test.tsx index 9a4d46e532416..880c8c4551bd2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.test.tsx @@ -11,6 +11,12 @@ import type { Alert } from '@kbn/alerting-types'; import { CellValue } from './render_cell'; import { TestProviders } from '../../../../common/mock'; import { getEmptyValue } from '../../../../common/components/empty_value'; +import { ALERT_RULE_PARAMETERS, ALERT_SEVERITY } from '@kbn/rule-data-utils'; +import { ICON_TEST_ID } from './kibana_alert_related_integrations_cell_renderer'; +import { useGetIntegrationFromPackageName } from '../../../hooks/alert_summary/use_get_integration_from_package_name'; +import { BADGE_TEST_ID } from './kibana_alert_severity_cell_renderer'; + +jest.mock('../../../hooks/alert_summary/use_get_integration_from_package_name'); describe('CellValue', () => { it('should handle missing field', () => { @@ -131,4 +137,42 @@ describe('CellValue', () => { expect(getByText('[object Object]')).toBeInTheDocument(); }); + + it('should use related integration renderer', () => { + (useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({ + integration: {}, + isLoading: false, + }); + + const alert: Alert = { + _id: '_id', + _index: '_index', + }; + const columnId = ALERT_RULE_PARAMETERS; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument(); + }); + + it('should use severity renderer', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + [ALERT_SEVERITY]: ['low'], + }; + const columnId = ALERT_SEVERITY; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId(BADGE_TEST_ID)).toBeInTheDocument(); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.tsx index 080b849d7edb4..a7de92212a0ac 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.tsx @@ -5,11 +5,14 @@ * 2.0. */ -import React, { memo, useMemo } from 'react'; +import React, { memo } from 'react'; import type { Alert } from '@kbn/alerting-types'; -import type { JsonValue } from '@kbn/utility-types'; -import { getOrEmptyTagFromValue } from '../../../../common/components/empty_value'; +import { ALERT_RULE_PARAMETERS, ALERT_SEVERITY } from '@kbn/rule-data-utils'; +import { BasicCellRenderer } from './basic_cell_renderer'; +import { KibanaAlertSeverityCellRenderer } from './kibana_alert_severity_cell_renderer'; +import { KibanaAlertRelatedIntegrationsCellRenderer } from './kibana_alert_related_integrations_cell_renderer'; +// guarantees that all cells will have their values vertically centered const styles = { display: 'flex', alignItems: 'center', height: '100%' }; export interface CellValueProps { @@ -29,36 +32,23 @@ export interface CellValueProps { * It will be soon improved to support custom renders for specific fields (like kibana.alert.rule.parameters and kibana.alert.severity). */ export const CellValue = memo(({ alert, columnId }: CellValueProps) => { - const displayValue: string | null = useMemo(() => { - const cellValues: string | number | JsonValue[] = alert[columnId]; + let component; - // Displays string as is. - // Joins values of array with more than one element. - // Returns null if the value is null. - // Return the string of the value otherwise. - if (typeof cellValues === 'string') { - return cellValues; - } else if (typeof cellValues === 'number') { - return cellValues.toString(); - } else if (Array.isArray(cellValues)) { - if (cellValues.length > 1) { - return cellValues.join(', '); - } else { - const value: JsonValue = cellValues[0]; - if (typeof value === 'string') { - return value; - } else if (value == null) { - return null; - } else { - return value.toString(); - } - } - } else { - return null; - } - }, [alert, columnId]); + switch (columnId) { + case ALERT_RULE_PARAMETERS: + component = ; + break; - return
{getOrEmptyTagFromValue(displayValue)}
; + case ALERT_SEVERITY: + component = ; + break; + + default: + component = ; + break; + } + + return
{component}
; }); CellValue.displayName = 'CellValue'; 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 bba449d9b140f..f1764890aca33 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 @@ -13,7 +13,13 @@ import { i18n } from '@kbn/i18n'; import { TableId } from '@kbn/securitysolution-data-table'; import { AlertsTable } from '@kbn/response-ops-alerts-table'; import type { AlertsTableProps } from '@kbn/response-ops-alerts-table/types'; -import { AlertConsumers } from '@kbn/rule-data-utils'; +import { + ALERT_RULE_NAME, + ALERT_RULE_PARAMETERS, + ALERT_SEVERITY, + AlertConsumers, + TIMESTAMP, +} from '@kbn/rule-data-utils'; import { ESQL_RULE_TYPE_ID, QUERY_RULE_TYPE_ID } from '@kbn/securitysolution-rules'; import type { EuiDataGridProps, @@ -48,26 +54,21 @@ const RULE_NAME_COLUMN = i18n.translate( { defaultMessage: 'Rule' } ); -const TIMESTAMP = '@timestamp'; -const RELATED_INTEGRATION = 'kibana.alert.rule.parameters'; -const SEVERITY = 'kibana.alert.severity'; -const RULE_NAME = 'kibana.alert.rule.name'; - const columns: EuiDataGridProps['columns'] = [ { id: TIMESTAMP, displayAsText: TIMESTAMP_COLUMN, }, { - id: RELATED_INTEGRATION, + id: ALERT_RULE_PARAMETERS, displayAsText: RELATION_INTEGRATION_COLUMN, }, { - id: SEVERITY, + id: ALERT_SEVERITY, displayAsText: SEVERITY_COLUMN, }, { - id: RULE_NAME, + id: ALERT_RULE_NAME, displayAsText: RULE_NAME_COLUMN, }, ]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_package_name.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_package_name.ts new file mode 100644 index 0000000000000..7c3bf90ee684f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_package_name.ts @@ -0,0 +1,51 @@ +/* + * 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 { useMemo } from 'react'; +import type { PackageListItem } from '@kbn/fleet-plugin/common'; +import { useFetchIntegrations } from './use_fetch_integrations'; + +export interface UseGetIntegrationFromRuleIdParams { + /** + * + */ + packageName: string | null; +} + +export interface UseGetIntegrationFromRuleIdResult { + /** + * List of integrations ready to be consumed by the IntegrationFilterButton component + */ + integration: PackageListItem | undefined; + /** + * True while rules are being fetched + */ + isLoading: boolean; +} + +/** + * + */ +export const useGetIntegrationFromPackageName = ({ + packageName, +}: UseGetIntegrationFromRuleIdParams): UseGetIntegrationFromRuleIdResult => { + // Fetch all packages + const { installedPackages, isLoading } = useFetchIntegrations(); + + const integration = useMemo( + () => installedPackages.find((installedPackage) => installedPackage.name === packageName), + [installedPackages, packageName] + ); + + return useMemo( + () => ({ + integration, + isLoading, + }), + [integration, isLoading] + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/utils/get_alert_field_value_as_string_or_null.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/get_alert_field_value_as_string_or_null.test.tsx new file mode 100644 index 0000000000000..2aa8b1f5c581d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/get_alert_field_value_as_string_or_null.test.tsx @@ -0,0 +1,102 @@ +/* + * 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 { Alert } from '@kbn/alerting-types'; +import { getAlertFieldValueAsStringOrNull } from './get_alert_field_value_as_string_or_null'; + +describe('getAlertFieldValueAsStringOrNull', () => { + it('should handle missing field', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: 'value1', + }; + const field = 'columnId'; + + const result = getAlertFieldValueAsStringOrNull(alert, field); + + expect(result).toBe(null); + }); + + it('should handle string value', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: 'value1', + }; + const field = 'field1'; + + const result = getAlertFieldValueAsStringOrNull(alert, field); + + expect(result).toEqual('value1'); + }); + + it('should handle a number value', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: 123, + }; + const field = 'field1'; + + const result = getAlertFieldValueAsStringOrNull(alert, field); + + expect(result).toEqual('123'); + }); + + it('should handle array of booleans', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: [true, false], + }; + const field = 'field1'; + + const result = getAlertFieldValueAsStringOrNull(alert, field); + + expect(result).toEqual('true, false'); + }); + + it('should handle array of numbers', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: [1, 2], + }; + const field = 'field1'; + + const result = getAlertFieldValueAsStringOrNull(alert, field); + + expect(result).toEqual('1, 2'); + }); + + it('should handle array of null', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: [null, null], + }; + const field = 'field1'; + + const result = getAlertFieldValueAsStringOrNull(alert, field); + + expect(result).toEqual(', '); + }); + + it('should join array of JsonObjects', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: [{ subField1: 'value1', subField2: 'value2' }], + }; + const field = 'field1'; + + const result = getAlertFieldValueAsStringOrNull(alert, field); + + expect(result).toEqual('[object Object]'); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/utils/get_alert_field_value_as_string_or_null.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/get_alert_field_value_as_string_or_null.ts new file mode 100644 index 0000000000000..09b540d1c3cb1 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/get_alert_field_value_as_string_or_null.ts @@ -0,0 +1,41 @@ +/* + * 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 { JsonValue } from '@kbn/utility-types'; +import type { Alert } from '@kbn/alerting-types'; + +/** + * Takes an Alert object and a field string as input and returns the value for the field as a string. + * If the value is already a string, return it. + * If the value is an array, join the values. + * If null the value is null. + * Return the string of the value otherwise. + */ +export const getAlertFieldValueAsStringOrNull = (alert: Alert, field: string): string | null => { + const cellValues: string | number | JsonValue[] = alert[field]; + + if (typeof cellValues === 'string') { + return cellValues; + } else if (typeof cellValues === 'number') { + return cellValues.toString(); + } else if (Array.isArray(cellValues)) { + if (cellValues.length > 1) { + return cellValues.join(', '); + } else { + const value: JsonValue = cellValues[0]; + if (typeof value === 'string') { + return value; + } else if (value == null) { + return null; + } else { + return value.toString(); + } + } + } else { + return null; + } +}; From 45a2a9c794b57443e25c0535fb4532b714367d44 Mon Sep 17 00:00:00 2001 From: PhilippeOberti Date: Thu, 17 Apr 2025 11:01:58 -0500 Subject: [PATCH 2/2] PR comments --- .../table/basic_cell_renderer.tsx | 2 +- ...ert_related_integrations_cell_renderer.tsx | 23 +++----- .../kibana_alert_severity_cell_renderer.tsx | 10 ++-- ...g_or_null.test.tsx => type_utils.test.tsx} | 53 ++++++++++++++++++- ...lue_as_string_or_null.ts => type_utils.ts} | 15 +++++- 5 files changed, 80 insertions(+), 23 deletions(-) rename x-pack/solutions/security/plugins/security_solution/public/detections/utils/{get_alert_field_value_as_string_or_null.test.tsx => type_utils.test.tsx} (66%) rename x-pack/solutions/security/plugins/security_solution/public/detections/utils/{get_alert_field_value_as_string_or_null.ts => type_utils.ts} (77%) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.tsx index 2d2d229ca4954..96add07d402c7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.tsx @@ -8,7 +8,7 @@ import React, { memo, useMemo } from 'react'; import type { Alert } from '@kbn/alerting-types'; import { getOrEmptyTagFromValue } from '../../../../common/components/empty_value'; -import { getAlertFieldValueAsStringOrNull } from '../../../utils/get_alert_field_value_as_string_or_null'; +import { getAlertFieldValueAsStringOrNull } from '../../../utils/type_utils'; export interface BasicCellRendererProps { /** diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.tsx index 9348eae561b42..3a8716855824c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.tsx @@ -12,6 +12,7 @@ import { EuiSkeletonText } from '@elastic/eui'; import type { Alert } from '@kbn/alerting-types'; import { ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils'; import { useGetIntegrationFromPackageName } from '../../../hooks/alert_summary/use_get_integration_from_package_name'; +import { getAlertFieldValueAsStringOrNull, isJsonObjectValue } from '../../../utils/type_utils'; export const SKELETON_TEST_ID = 'alert-summary-table-related-integrations-cell-renderer-skeleton'; export const ICON_TEST_ID = 'alert-summary-table-related-integrations-cell-renderer-icon'; @@ -19,6 +20,8 @@ export const ICON_TEST_ID = 'alert-summary-table-related-integrations-cell-rende const RELATED_INTEGRATIONS_FIELD = 'related_integrations'; const PACKAGE_FIELD = 'package'; +// function is_string(value: unknown): value is string {} + export interface KibanaAlertRelatedIntegrationsCellRendererProps { /** * Alert data passed from the renderCellValue callback via the AlertWithLegacyFormats interface @@ -38,26 +41,12 @@ export const KibanaAlertRelatedIntegrationsCellRenderer = memo( if (Array.isArray(values) && values.length === 1) { const value: JsonValue = values[0]; - if ( - !value || - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' || - Array.isArray(value) - ) - return null; + if (!isJsonObjectValue(value)) return null; const relatedIntegration = value[RELATED_INTEGRATIONS_FIELD]; - if ( - !relatedIntegration || - typeof relatedIntegration === 'string' || - typeof relatedIntegration === 'number' || - typeof relatedIntegration === 'boolean' || - Array.isArray(relatedIntegration) - ) - return 'splunk'; // null; + if (!isJsonObjectValue(relatedIntegration)) return null; - return relatedIntegration[PACKAGE_FIELD] as string; + return getAlertFieldValueAsStringOrNull(relatedIntegration as Alert, PACKAGE_FIELD); } return null; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_severity_cell_renderer.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_severity_cell_renderer.tsx index af80d5d7f4480..2e506c2d4a6d9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_severity_cell_renderer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_severity_cell_renderer.tsx @@ -14,6 +14,12 @@ import { getSeverityColor } from '../../alerts_kpis/severity_level_panel/helpers export const BADGE_TEST_ID = 'alert-summary-table-severity-cell-renderer'; +/** + * Return the same string with the first letter capitalized + */ +const capitalizeFirstLetter = (value: string): string => + String(value).charAt(0).toUpperCase() + String(value).slice(1); + export interface KibanaAlertSeverityCellRendererProps { /** * Alert data passed from the renderCellValue callback via the AlertWithLegacyFormats interface @@ -34,9 +40,7 @@ export const KibanaAlertSeverityCellRenderer = memo( if (Array.isArray(values) && values.length === 1) { const value: JsonValue = values[0]; - return value && typeof value === 'string' - ? String(value).charAt(0).toUpperCase() + String(value).slice(1) - : null; + return value && typeof value === 'string' ? capitalizeFirstLetter(value) : null; } return null; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/utils/get_alert_field_value_as_string_or_null.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/type_utils.test.tsx similarity index 66% rename from x-pack/solutions/security/plugins/security_solution/public/detections/utils/get_alert_field_value_as_string_or_null.test.tsx rename to x-pack/solutions/security/plugins/security_solution/public/detections/utils/type_utils.test.tsx index 2aa8b1f5c581d..943b64f5a883c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/utils/get_alert_field_value_as_string_or_null.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/type_utils.test.tsx @@ -6,7 +6,8 @@ */ import type { Alert } from '@kbn/alerting-types'; -import { getAlertFieldValueAsStringOrNull } from './get_alert_field_value_as_string_or_null'; +import { getAlertFieldValueAsStringOrNull, isJsonObjectValue } from './type_utils'; +import type { JsonValue } from '@kbn/utility-types'; describe('getAlertFieldValueAsStringOrNull', () => { it('should handle missing field', () => { @@ -100,3 +101,53 @@ describe('getAlertFieldValueAsStringOrNull', () => { expect(result).toEqual('[object Object]'); }); }); + +describe('isJsonObjectValue', () => { + it('should return true for JsonObject', () => { + const value: JsonValue = { test: 'value' }; + + const result = isJsonObjectValue(value); + + expect(result).toBe(true); + }); + + it('should return false for null', () => { + const value: JsonValue = null; + + const result = isJsonObjectValue(value); + + expect(result).toBe(false); + }); + + it('should return false for string', () => { + const value: JsonValue = 'test'; + + const result = isJsonObjectValue(value); + + expect(result).toBe(false); + }); + + it('should return false for number', () => { + const value: JsonValue = 123; + + const result = isJsonObjectValue(value); + + expect(result).toBe(false); + }); + + it('should return false for boolean', () => { + const value: JsonValue = true; + + const result = isJsonObjectValue(value); + + expect(result).toBe(false); + }); + + it('should return false for array', () => { + const value: JsonValue = ['test', 123, true]; + + const result = isJsonObjectValue(value); + + expect(result).toBe(false); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/utils/get_alert_field_value_as_string_or_null.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/type_utils.ts similarity index 77% rename from x-pack/solutions/security/plugins/security_solution/public/detections/utils/get_alert_field_value_as_string_or_null.ts rename to x-pack/solutions/security/plugins/security_solution/public/detections/utils/type_utils.ts index 09b540d1c3cb1..f05bdef660eed 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/utils/get_alert_field_value_as_string_or_null.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/type_utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { JsonValue } from '@kbn/utility-types'; +import type { JsonObject, JsonValue } from '@kbn/utility-types'; import type { Alert } from '@kbn/alerting-types'; /** @@ -39,3 +39,16 @@ export const getAlertFieldValueAsStringOrNull = (alert: Alert, field: string): s return null; } }; + +/** + * Guaratees that the value is of type JsonObject + */ +export const isJsonObjectValue = (value: JsonValue): value is JsonObject => { + return ( + value != null && + typeof value !== 'string' && + typeof value !== 'number' && + typeof value !== 'boolean' && + !Array.isArray(value) + ); +};