diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx index caf864d4b189b..4c2982b5db9b7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx @@ -111,11 +111,12 @@ export const isEvenEqlSequence = (event: Ecs): boolean => { }; /** Return eventType raw or signal or eql */ export const getEventType = (event: Ecs): Omit => { - if (!isEmpty(event?.kibana?.alert?.rule?.uuid)) { - return 'signal'; - } else if (!isEmpty(event?.eql?.parentId)) { + if (!isEmpty(event?.eql?.parentId)) { return 'eql'; + } else if (!isEmpty(event?.kibana?.alert?.rule?.uuid)) { + return 'signal'; } + return 'raw'; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx index 25472d7d4f277..746b3b1ceb1dd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx @@ -557,8 +557,9 @@ describe('query tab with unified timeline', () => { }); const messageColumnIndex = - customColumnOrder.findIndex((header) => header.id === 'message') + 3; - // 3 is the offset for additional leading columns on left + customColumnOrder.findIndex((header) => header.id === 'message') + + // offset for additional leading columns on left + 4; expect(container.querySelector('[data-gridcell-column-id="message"]')).toHaveAttribute( 'data-gridcell-column-index', diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/get_event_type_row_classname.ts b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/get_event_type_row_classname.ts index b6058af189c9d..97417303145a8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/get_event_type_row_classname.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/get_event_type_row_classname.ts @@ -11,12 +11,12 @@ import { getEventType, isEvenEqlSequence, isEventBuildingBlockType } from '../.. export const getEventTypeRowClassName = (ecsData: TimelineItem['ecs']) => { const eventType = getEventType(ecsData); const eventTypeClassName = - eventType === 'raw' - ? 'rawEvent' - : eventType === 'eql' + eventType === 'eql' ? isEvenEqlSequence(ecsData) ? 'eqlSequence' : 'eqlNonSequence' + : eventType === 'raw' + ? 'rawEvent' : 'nonRawEvent'; const buildingBlockTypeClassName = isEventBuildingBlockType(ecsData) ? 'buildingBlockType' : ''; diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/get_row_indicator.test.ts b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/get_row_indicator.test.ts new file mode 100644 index 0000000000000..3f7f3fe8ec6f7 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/get_row_indicator.test.ts @@ -0,0 +1,105 @@ +/* + * 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 { DataTableRecord } from '@kbn/discover-utils'; +import { getTimelineRowTypeIndicator } from './get_row_indicator'; +import type { EuiThemeComputed } from '@elastic/eui'; + +const mockEuiTheme = { + colors: { + primary: 'primary', + accent: 'accent', + warning: 'warning', + lightShade: 'lightShade', + }, +} as EuiThemeComputed; + +describe('getTimelineRowTypeIndicator', () => { + describe('Alert', () => { + it('should return correct label and color for EQL Event', () => { + const row = { + flattened: { + 'event.kind': 'signal', + 'eql.parentId': '123', + 'eql.sequenceNumber': '1-3', + }, + } as unknown as DataTableRecord; + const rowIndicator = getTimelineRowTypeIndicator(row, mockEuiTheme); + expect(rowIndicator).toEqual({ + color: 'accent', + label: 'EQL Sequence', + }); + }); + it('should return correct label and color for non-EQL Event', () => { + const row = { + flattened: { + 'event.kind': 'signal', + }, + } as unknown as DataTableRecord; + const rowIndicator = getTimelineRowTypeIndicator(row, mockEuiTheme); + expect(rowIndicator).toEqual({ + color: 'warning', + label: 'Alert', + }); + }); + }); + + describe('Event', () => { + it('should return correct label and color for EQL Event', () => { + const row = { + flattened: { + 'eql.parentId': '123', + 'eql.sequenceNumber': '1-3', + }, + } as unknown as DataTableRecord; + const rowIndicator = getTimelineRowTypeIndicator(row, mockEuiTheme); + expect(rowIndicator).toEqual({ + color: 'accent', + label: 'EQL Sequence', + }); + }); + it('should return correct label and color for non-EQL Event', () => { + const row = { + flattened: {}, + } as unknown as DataTableRecord; + const rowIndicator = getTimelineRowTypeIndicator(row, mockEuiTheme); + expect(rowIndicator).toMatchObject({ + color: 'lightShade', + label: 'Event', + }); + }); + }); + + describe('EQL Event Type', () => { + it('should return correct label and color for Even EQL Sequence', () => { + const row = { + flattened: { + 'eql.parentId': '123', + 'eql.sequenceNumber': '2-4', + }, + } as unknown as DataTableRecord; + const rowIndicator = getTimelineRowTypeIndicator(row, mockEuiTheme); + expect(rowIndicator).toEqual({ + color: 'primary', + label: 'EQL Sequence', + }); + }); + it('should return correct label and color for Non-Even EQL Sequence', () => { + const row = { + flattened: { + 'eql.parentId': '123', + 'eql.sequenceNumber': '1-4', + }, + } as unknown as DataTableRecord; + const rowIndicator = getTimelineRowTypeIndicator(row, mockEuiTheme); + expect(rowIndicator).toEqual({ + color: 'accent', + label: 'EQL Sequence', + }); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/get_row_indicator.ts b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/get_row_indicator.ts new file mode 100644 index 0000000000000..074077597c521 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/get_row_indicator.ts @@ -0,0 +1,49 @@ +/* + * 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 { EuiThemeComputed } from '@elastic/eui'; +import type { DataTableRecord } from '@kbn/discover-utils'; +import { getFieldValue } from '@kbn/discover-utils'; +import type { UnifiedDataTableProps } from '@kbn/unified-data-table'; +import { isEmpty } from 'lodash'; + +export const getTimelineRowTypeIndicator: NonNullable = ( + row: DataTableRecord, + euiTheme: EuiThemeComputed +) => { + const isAlert = getFieldValue(row, 'event.kind') === 'signal'; + + const isEql = + !isEmpty(getFieldValue(row, 'eql.parentId')) && + !isEmpty(getFieldValue(row, 'eql.sequenceNumber')); + + if (isEql) { + const sequenceNumber = ((getFieldValue(row, 'eql.sequenceNumber') as string) ?? '').split( + '-' + )[0]; + + const isEvenSequence = parseInt(sequenceNumber, 10) % 2 === 0; + + return { + /* alternating colors to differentiate consecutive sequences */ + color: isEvenSequence ? euiTheme.colors.primary : euiTheme.colors.accent, + label: 'EQL Sequence', + }; + } + + if (isAlert) { + return { + color: euiTheme.colors.warning, + label: 'Alert', + }; + } + + return { + color: euiTheme.colors.lightShade, + label: 'Event', + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx index 32f373000f78b..35bed4ddd804b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx @@ -49,6 +49,7 @@ import { TimelineEventDetailRow } from './timeline_event_detail_row'; import { CustomTimelineDataGridBody } from './custom_timeline_data_grid_body'; import { TIMELINE_EVENT_DETAIL_ROW_ID } from '../../body/constants'; import { DocumentEventTypes } from '../../../../../common/lib/telemetry/types'; +import { getTimelineRowTypeIndicator } from './get_row_indicator'; export const SAMPLE_SIZE_SETTING = 500; const DataGridMemoized = React.memo(UnifiedDataTable); @@ -423,6 +424,7 @@ export const TimelineDataTableComponent: React.FC = memo( trailingControlColumns={finalTrailControlColumns} externalControlColumns={leadingControlColumns} onUpdatePageIndex={onUpdatePageIndex} + getRowIndicator={getTimelineRowTypeIndicator} /> diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx index 7d9bde02259a4..872f3c760f38b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx @@ -233,7 +233,7 @@ describe('unified timeline', () => { }); expect( container.querySelector(`[data-gridcell-column-id="${field.name}"]`) - ).toHaveAttribute('data-gridcell-column-index', '3'); + ).toHaveAttribute('data-gridcell-column-index', '4'); expect( container.querySelector(`[data-gridcell-column-id="${field.name}"]`) @@ -268,7 +268,7 @@ describe('unified timeline', () => { }); expect( container.querySelector(`[data-gridcell-column-id="${field.name}"]`) - ).toHaveAttribute('data-gridcell-column-index', '3'); + ).toHaveAttribute('data-gridcell-column-index', '4'); expect( container.querySelector(`[data-gridcell-column-id="${field.name}"]`) @@ -482,7 +482,7 @@ describe('unified timeline', () => { }); expect( container.querySelector(`[data-gridcell-column-id="${field.name}"]`) - ).toHaveAttribute('data-gridcell-column-index', '3'); + ).toHaveAttribute('data-gridcell-column-index', '4'); expect( container.querySelector(`[data-gridcell-column-id="${field.name}"]`) diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/styles.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/styles.tsx index 49c33774572e5..f4a30bbcc13f8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/styles.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/styles.tsx @@ -74,8 +74,19 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = '' .udtTimeline [data-gridcell-column-id|='select'] { border-right: none; } - .udtTimeline [data-gridcell-column-id|='openDetails'] .euiDataGridRowCell__contentByHeight { - margin-top: 3px; + .udtTimeline [data-gridcell-column-id|='openDetails'] { + /* custom row height based on number of lines */ + .euiDataGridRowCell__content--lineCountHeight, + + /* auto row height */ + .euiDataGridRowCell__content--autoHeight { + margin-top: 9px; + } + + /* single row height */ + .euiDataGridRowCell__content--defaultHeight { + margin-top: 3px; + } } .udtTimeline @@ -90,11 +101,24 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = '' overflow: visible; } - .udtTimeline [data-gridcell-column-id|='select'] .euiDataGridRowCell__contentByHeight { - margin-top: 5px; + .udtTimeline [data-gridcell-column-id|='select'] { + /* custom row height based on number of lines */ + .euiDataGridRowCell__content--lineCountHeight, + + /* auto row height */ + .euiDataGridRowCell__content--autoHeight { + margin-top: 6px; + } + + /* single row height */ + .euiDataGridRowCell__content--defaultHeight { + margin-top: 3px; + } } .udtTimeline + [data-gridcell-column-id|='select'] + .udtTimeline .euiDataGridRow:hover .euiDataGridRowCell--lastColumn.euiDataGridRowCell--controlColumn { ${({ theme }) => `background-color: ${theme.eui.colorLightShade};`}; @@ -120,7 +144,6 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = '' } .udtTimeline .euiDataGridRow:has(.eqlSequence), .udtTimeline .euiDataGridRow.eqlSequence { - .euiDataGridRowCell--firstColumn, .euiDataGridRowCell--controlColumn.euiDataGridRowCell--lastColumn, .udt--customRow { ${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorPrimary}`}; @@ -135,7 +158,6 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = '' } .udtTimeline .euiDataGridRow:has(.eqlNonSequence), .udtTimeline .euiDataGridRow.eqlNonSequence { - .euiDataGridRowCell--firstColumn, .euiDataGridRowCell--controlColumn.euiDataGridRowCell--lastColumn, .udt--customRow { ${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorAccent};`} @@ -150,7 +172,6 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = '' } .udtTimeline .euiDataGridRow:has(.nonRawEvent), .udtTimeline .euiDataGridRow.nonRawEvent { - .euiDataGridRowCell--firstColumn, .euiDataGridRowCell--controlColumn.euiDataGridRowCell--lastColumn, .udt--customRow { ${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorWarning};`} @@ -158,7 +179,6 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = '' } .udtTimeline .euiDataGridRow:has(.rawEvent), .udtTimeline .euiDataGridRow.rawEvent { - .euiDataGridRowCell--firstColumn, .euiDataGridRowCell--controlColumn.euiDataGridRowCell--lastColumn, .udt--customRow { ${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorLightShade};`}