diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 732097aedbc2b..fe8ec46b752c5 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -1947,6 +1947,11 @@ const mockTimelineModelColumns: TimelineModel['columns'] = [ id: 'user.name', initialWidth: 180, }, + { + columnHeaderType: 'not-filtered', + id: 'kibana.alert.workflow_assignee_ids', + initialWidth: 180, + }, ]; export const mockTimelineModel: TimelineModel = { activeTab: TimelineTabs.query, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx index b4b0a07fd7ab2..fc5d91bcaf8e5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx @@ -6,16 +6,78 @@ */ import type { ExistsFilter, Filter } from '@kbn/es-query'; +import { tableDefaults } from '@kbn/securitysolution-data-table'; +import { createLicenseServiceMock } from '../../../../common/license/mocks'; import { buildAlertAssigneesFilter, buildAlertsFilter, buildAlertStatusesFilter, buildAlertStatusFilter, buildThreatMatchFilter, + getAlertsDefaultModel, + getAlertsPreviewDefaultModel, } from './default_config'; jest.mock('./actions'); +const basicBaseColumns = [ + { + columnHeaderType: 'not-filtered', + displayAsText: 'Severity', + id: 'kibana.alert.severity', + initialWidth: 105, + }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Risk Score', + id: 'kibana.alert.risk_score', + initialWidth: 100, + }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Reason', + id: 'kibana.alert.reason', + initialWidth: 450, + }, + { columnHeaderType: 'not-filtered', id: 'host.name' }, + { columnHeaderType: 'not-filtered', id: 'user.name' }, + { columnHeaderType: 'not-filtered', id: 'process.name' }, + { columnHeaderType: 'not-filtered', id: 'file.name' }, + { columnHeaderType: 'not-filtered', id: 'source.ip' }, + { columnHeaderType: 'not-filtered', id: 'destination.ip' }, +]; + +const platinumBaseColumns = [ + { + columnHeaderType: 'not-filtered', + displayAsText: 'Severity', + id: 'kibana.alert.severity', + initialWidth: 105, + }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Risk Score', + id: 'kibana.alert.risk_score', + initialWidth: 100, + }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Reason', + id: 'kibana.alert.reason', + initialWidth: 450, + }, + { columnHeaderType: 'not-filtered', id: 'host.name' }, + { columnHeaderType: 'not-filtered', id: 'host.risk.calculated_level' }, + { columnHeaderType: 'not-filtered', id: 'user.name' }, + { columnHeaderType: 'not-filtered', id: 'user.risk.calculated_level' }, + { columnHeaderType: 'not-filtered', id: 'kibana.alert.host.criticality_level' }, + { columnHeaderType: 'not-filtered', id: 'kibana.alert.user.criticality_level' }, + { columnHeaderType: 'not-filtered', id: 'process.name' }, + { columnHeaderType: 'not-filtered', id: 'file.name' }, + { columnHeaderType: 'not-filtered', id: 'source.ip' }, + { columnHeaderType: 'not-filtered', id: 'destination.ip' }, +]; + describe('alerts default_config', () => { describe('buildAlertsRuleIdFilter', () => { test('given a rule id this will return an array with a single filter', () => { @@ -200,6 +262,122 @@ describe('alerts default_config', () => { }); }); + describe('getAlertsDefaultModel', () => { + test('returns correct model for Basic license', () => { + const licenseServiceMock = createLicenseServiceMock(); + licenseServiceMock.isPlatinumPlus.mockReturnValue(false); + const model = getAlertsDefaultModel(licenseServiceMock); + + const expected = { + ...tableDefaults, + showCheckboxes: true, + columns: [ + { columnHeaderType: 'not-filtered', id: '@timestamp', initialWidth: 200 }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Rule', + id: 'kibana.alert.rule.name', + initialWidth: 180, + linkField: 'kibana.alert.rule.uuid', + }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Assignees', + id: 'kibana.alert.workflow_assignee_ids', + initialWidth: 190, + }, + ...basicBaseColumns, + ], + }; + expect(model).toEqual(expected); + }); + + test('returns correct model for Platinum license', () => { + const licenseServiceMock = createLicenseServiceMock(); + const model = getAlertsDefaultModel(licenseServiceMock); + + const expected = { + ...tableDefaults, + showCheckboxes: true, + columns: [ + { columnHeaderType: 'not-filtered', id: '@timestamp', initialWidth: 200 }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Rule', + id: 'kibana.alert.rule.name', + initialWidth: 180, + linkField: 'kibana.alert.rule.uuid', + }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Assignees', + id: 'kibana.alert.workflow_assignee_ids', + initialWidth: 190, + }, + ...platinumBaseColumns, + ], + }; + expect(model).toEqual(expected); + }); + }); + + describe('getAlertsPreviewDefaultModel', () => { + test('returns correct model for Basic license', () => { + const licenseServiceMock = createLicenseServiceMock(); + licenseServiceMock.isPlatinumPlus.mockReturnValue(false); + const model = getAlertsPreviewDefaultModel(licenseServiceMock); + + const expected = { + ...tableDefaults, + showCheckboxes: false, + defaultColumns: [ + { columnHeaderType: 'not-filtered', id: 'kibana.alert.original_time', initialWidth: 200 }, + ...basicBaseColumns, + ], + columns: [ + { columnHeaderType: 'not-filtered', id: 'kibana.alert.original_time', initialWidth: 200 }, + ...basicBaseColumns, + ], + sort: [ + { + columnId: 'kibana.alert.original_time', + columnType: 'date', + esTypes: ['date'], + sortDirection: 'desc', + }, + ], + }; + expect(model).toEqual(expected); + }); + + test('returns correct model for Platinum license', () => { + const licenseServiceMock = createLicenseServiceMock(); + const model = getAlertsPreviewDefaultModel(licenseServiceMock); + + const expected = { + ...tableDefaults, + showCheckboxes: false, + defaultColumns: [ + { columnHeaderType: 'not-filtered', id: 'kibana.alert.original_time', initialWidth: 200 }, + ...platinumBaseColumns, + ], + columns: [ + { columnHeaderType: 'not-filtered', id: 'kibana.alert.original_time', initialWidth: 200 }, + ...platinumBaseColumns, + ], + sort: [ + { + columnId: 'kibana.alert.original_time', + columnType: 'date', + esTypes: ['date'], + sortDirection: 'desc', + }, + ], + }; + expect(model).toEqual(expected); + }); + }); + // TODO: move these tests to ../timelines/components/timeline/body/events/event_column_view.tsx // describe.skip('getAlertActions', () => { // let setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 6065e617c1254..9db07f4824652 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -192,7 +192,7 @@ export const getAlertsDefaultModel = (license?: LicenseService): SubsetDataTable export const getAlertsPreviewDefaultModel = (license?: LicenseService): SubsetDataTableModel => ({ ...getAlertsDefaultModel(license), - columns: getColumns(license), + columns: getRulePreviewColumns(license), defaultColumns: getRulePreviewColumns(license), sort: [ { diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts index 384c6bf955e51..b22bf5e2ed429 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts @@ -25,6 +25,13 @@ import { DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH, } from './translations'; +export const assigneesColumn: ColumnHeaderOptions = { + columnHeaderType: defaultColumnHeaderType, + displayAsText: i18n.ALERTS_HEADERS_ASSIGNEES, + id: 'kibana.alert.workflow_assignee_ids', + initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, +}; + const getBaseColumns = ( license?: LicenseService ): Array< @@ -32,12 +39,6 @@ const getBaseColumns = ( > => { const isPlatinumPlus = license?.isPlatinumPlus?.() ?? false; return [ - { - columnHeaderType: defaultColumnHeaderType, - displayAsText: i18n.ALERTS_HEADERS_ASSIGNEES, - id: 'kibana.alert.workflow_assignee_ids', - initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, - }, { columnHeaderType: defaultColumnHeaderType, displayAsText: i18n.ALERTS_HEADERS_SEVERITY, @@ -131,6 +132,7 @@ export const getColumns = ( initialWidth: DEFAULT_COLUMN_MIN_WIDTH, linkField: 'kibana.alert.rule.uuid', }, + assigneesColumn, ...getBaseColumns(license), ]; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts index f715169e2abb9..94f03268a23b2 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts @@ -16,6 +16,7 @@ import { addTableInStorage, migrateAlertTableStateToTriggerActionsState, migrateTriggerActionsVisibleColumnsAlertTable88xTo89, + addAssigneesSpecsToSecurityDataTableIfNeeded, } from '.'; import { mockDataTableModel, createSecuritySolutionStorageMock } from '../../../common/mock'; @@ -566,6 +567,12 @@ describe('SiemLocalStorage', () => { { columnHeaderType: 'not-filtered', id: 'file.name' }, { columnHeaderType: 'not-filtered', id: 'source.ip' }, { columnHeaderType: 'not-filtered', id: 'destination.ip' }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Assignees', + id: 'kibana.alert.workflow_assignee_ids', + initialWidth: 190, + }, ], defaultColumns: [ { columnHeaderType: 'not-filtered', id: '@timestamp', initialWidth: 200 }, @@ -600,6 +607,12 @@ describe('SiemLocalStorage', () => { { columnHeaderType: 'not-filtered', id: 'file.name' }, { columnHeaderType: 'not-filtered', id: 'source.ip' }, { columnHeaderType: 'not-filtered', id: 'destination.ip' }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Assignees', + id: 'kibana.alert.workflow_assignee_ids', + initialWidth: 190, + }, ], dataViewId: 'security-solution-default', deletedEventIds: [], @@ -1527,4 +1540,88 @@ describe('SiemLocalStorage', () => { ).toBeNull(); }); }); + + describe('addMissingColumnsToSecurityDataTable', () => { + it('should add missing "Assignees" column specs', () => { + const dataTableState: DataTableState['dataTable']['tableById'] = { + 'alerts-page': { + columns: [{ columnHeaderType: 'not-filtered', id: '@timestamp', initialWidth: 200 }], + defaultColumns: [ + { columnHeaderType: 'not-filtered', id: 'host.name' }, + { columnHeaderType: 'not-filtered', id: 'user.name' }, + { columnHeaderType: 'not-filtered', id: 'process.name' }, + ], + isLoading: false, + queryFields: [], + dataViewId: 'security-solution-default', + deletedEventIds: [], + expandedDetail: {}, + filters: [], + indexNames: ['.alerts-security.alerts-default'], + isSelectAllChecked: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + loadingEventIds: [], + showCheckboxes: true, + sort: [ + { + columnId: '@timestamp', + columnType: 'date', + esTypes: ['date'], + sortDirection: 'desc', + }, + ], + graphEventId: undefined, + selectedEventIds: {}, + sessionViewConfig: null, + selectAll: false, + id: 'alerts-page', + title: '', + initialized: true, + updated: 1665943295913, + totalCount: 0, + viewMode: VIEW_SELECTION.gridView, + additionalFilters: { + showBuildingBlockAlerts: false, + showOnlyThreatIndicatorAlerts: false, + }, + }, + }; + storage.set(LOCAL_STORAGE_TABLE_KEY, dataTableState); + migrateAlertTableStateToTriggerActionsState(storage, dataTableState); + migrateTriggerActionsVisibleColumnsAlertTable88xTo89(storage); + + const expectedColumns = [ + { columnHeaderType: 'not-filtered', id: '@timestamp', initialWidth: 200 }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Assignees', + id: 'kibana.alert.workflow_assignee_ids', + initialWidth: 190, + }, + ]; + const expectedDefaultColumns = [ + { columnHeaderType: 'not-filtered', id: 'host.name' }, + { columnHeaderType: 'not-filtered', id: 'user.name' }, + { columnHeaderType: 'not-filtered', id: 'process.name' }, + { + columnHeaderType: 'not-filtered', + displayAsText: 'Assignees', + id: 'kibana.alert.workflow_assignee_ids', + initialWidth: 190, + }, + ]; + + addAssigneesSpecsToSecurityDataTableIfNeeded(storage, dataTableState); + + expect(dataTableState['alerts-page'].columns).toMatchObject(expectedColumns); + expect(dataTableState['alerts-page'].defaultColumns).toMatchObject(expectedDefaultColumns); + + const tableKey = 'detection-engine-alert-table-securitySolution-alerts-page-gridView'; + expect(storage.get(tableKey)).toMatchObject({ + columns: expectedColumns, + visibleColumns: expectedColumns.map((col) => col.id), + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx index bab84294e0a67..8191f800ff934 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx @@ -12,8 +12,9 @@ import type { DataTableModel, TableIdLiteral, } from '@kbn/securitysolution-data-table'; -import { TableId } from '@kbn/securitysolution-data-table'; +import { tableEntity, TableEntityType, TableId } from '@kbn/securitysolution-data-table'; import type { ColumnHeaderOptions } from '@kbn/timelines-plugin/common'; +import { assigneesColumn } from '../../../detections/configurations/security_solution_detections/columns'; import { ALERTS_TABLE_REGISTRY_CONFIG_IDS, VIEW_SELECTION } from '../../../../common/constants'; import type { DataTablesStorage } from './types'; import { useKibana } from '../../../common/lib/kibana'; @@ -195,6 +196,90 @@ export const migrateColumnLabelToDisplayAsText = ( : {}), }); +/** + * Adds "Assignees" column and makes it visible in alerts table + */ +const addAssigneesColumnToAlertsTable = (storage: Storage) => { + const localStorageKeys = [ + `detection-engine-alert-table-${ALERTS_TABLE_REGISTRY_CONFIG_IDS.ALERTS_PAGE}-gridView`, + `detection-engine-alert-table-${ALERTS_TABLE_REGISTRY_CONFIG_IDS.RULE_DETAILS}-gridView`, + ]; + + localStorageKeys.forEach((key) => { + const alertTableData = storage.get(key); + if (!alertTableData) { + return; + } + // Make "Assignees" field selected in the table + if ('columns' in alertTableData) { + let updatedAlertsTableState = false; + const columns = + alertTableData.columns as DataTableState['dataTable']['tableById'][string]['columns']; + const hasAssigneesColumn = columns.findIndex((col) => col.id === assigneesColumn.id) !== -1; + if (!hasAssigneesColumn) { + // Insert "Assignees" column at the index 1 to mimic behaviour of adding field to alerts table + alertTableData.columns.splice(1, 0, assigneesColumn); + updatedAlertsTableState = true; + } + // Make "Assignees" column visible in the table + if ('visibleColumns' in alertTableData) { + const visibleColumns = alertTableData.visibleColumns as string[]; + const assigneesColumnExists = + visibleColumns.findIndex((col) => col === assigneesColumn.id) !== -1; + if (!assigneesColumnExists) { + alertTableData.visibleColumns.splice(1, 0, assigneesColumn.id); + updatedAlertsTableState = true; + } + } + if (updatedAlertsTableState) { + storage.set(key, alertTableData); + } + } + }); +}; + +/** + * Adds "Assignees" column specs to table data model + */ +export const addAssigneesSpecsToSecurityDataTableIfNeeded = ( + storage: Storage, + dataTableState: DataTableState['dataTable']['tableById'] +) => { + // Add "Assignees" column specs to the table data model + let updatedTableModel = false; + for (const [tableId, tableModel] of Object.entries(dataTableState)) { + // Only add "Assignees" column specs to alerts tables + if (tableEntity[tableId as TableId] !== TableEntityType.alert) { + // eslint-disable-next-line no-continue + continue; + } + + // We added a new base column for "Assignees" in 8.12 + // In order to show correct custom header label after user upgrades to 8.12 we need to make sure the appropriate specs are in the table model. + const columns = tableModel.columns; + if (Array.isArray(columns)) { + const hasAssigneesColumn = columns.findIndex((col) => col.id === assigneesColumn.id) !== -1; + if (!hasAssigneesColumn) { + updatedTableModel = true; + tableModel.columns.push(assigneesColumn); + } + } + const defaultColumns = tableModel.defaultColumns; + if (defaultColumns) { + const hasAssigneesColumn = + defaultColumns.findIndex((col) => col.id === assigneesColumn.id) !== -1; + if (!hasAssigneesColumn) { + updatedTableModel = true; + tableModel.defaultColumns.push(assigneesColumn); + } + } + } + if (updatedTableModel) { + storage.set(LOCAL_STORAGE_TABLE_KEY, dataTableState); + addAssigneesColumnToAlertsTable(storage); + } +}; + export const getDataTablesInStorageByIds = (storage: Storage, tableIds: TableIdLiteral[]) => { let allDataTables = storage.get(LOCAL_STORAGE_TABLE_KEY); const legacyTimelineTables = storage.get(LOCAL_STORAGE_TIMELINE_KEY_LEGACY); @@ -209,6 +294,7 @@ export const getDataTablesInStorageByIds = (storage: Storage, tableIds: TableIdL migrateAlertTableStateToTriggerActionsState(storage, allDataTables); migrateTriggerActionsVisibleColumnsAlertTable88xTo89(storage); + addAssigneesSpecsToSecurityDataTableIfNeeded(storage, allDataTables); return tableIds.reduce((acc, tableId) => { const tableModel = allDataTables[tableId];