From 440fee718d353aebe2615045f3e2df8f65056485 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Wed, 13 Nov 2024 21:52:00 +0100 Subject: [PATCH] [Security Solution] Migration of Alert Page controls for non-default Spaces. (#200058) ## Summary Recently, we created a PR to migrate the alert page filters controls to `8.16`. Unfortunately, it does not do migration for non-default spaces so any users upgrading to `8.16` will face the issue where Alert page errors out as shown in below screenshot. ![grafik](https://github.com/user-attachments/assets/ffee1c2d-4aa2-44a4-96c9-68053fb1cf63) ## Desk Testing 1. Checkout to `v8.15` branch by running `git checkout 8.15`. 2. Create a new space and go to that space. 3. Go to the alert page and do some modifications to the page controls. This store `v8.15` page controls in local storage. - You can, for example, delete one page control. - Change selected value for one page control. - Additionally, you can also add a custom control. 4. Checkout `main` now and repeat the above steps. 5. Your changes should be retained on the alert page and there should not be any error. --- .../public/detections/index.ts | 5 +- .../public/detections/migrations.ts | 19 +-- .../security_solution/public/plugin.tsx | 3 +- .../migrate_alert_page_contorls.test.ts | 111 +++++++++++++----- .../migrate_alert_page_controls.ts | 13 +- .../plugins/security_solution/public/types.ts | 2 +- 6 files changed, 110 insertions(+), 43 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/index.ts b/x-pack/plugins/security_solution/public/detections/index.ts index 77d131377f9f..1f9ce0b6fe01 100644 --- a/x-pack/plugins/security_solution/public/detections/index.ts +++ b/x-pack/plugins/security_solution/public/detections/index.ts @@ -12,6 +12,7 @@ import { getDataTablesInStorageByIds } from '../timelines/containers/local_stora import { routes } from './routes'; import type { SecuritySubPlugin } from '../app/types'; import { runDetectionMigrations } from './migrations'; +import type { StartPlugins } from '../types'; export const DETECTIONS_TABLE_IDS: TableIdLiteral[] = [ TableId.alertsOnRuleDetailsPage, @@ -21,8 +22,8 @@ export const DETECTIONS_TABLE_IDS: TableIdLiteral[] = [ export class Detections { public setup() {} - public start(storage: Storage): SecuritySubPlugin { - runDetectionMigrations(); + public async start(storage: Storage, plugins: StartPlugins): Promise { + await runDetectionMigrations(storage, plugins); return { storageDataTables: { diff --git a/x-pack/plugins/security_solution/public/detections/migrations.ts b/x-pack/plugins/security_solution/public/detections/migrations.ts index 81009f63747a..324abc443cb0 100644 --- a/x-pack/plugins/security_solution/public/detections/migrations.ts +++ b/x-pack/plugins/security_solution/public/detections/migrations.ts @@ -5,16 +5,21 @@ * 2.0. */ -import { Storage } from '@kbn/kibana-utils-plugin/public'; +import type { Storage } from '@kbn/kibana-utils-plugin/public'; import { migrateAlertPageControlsTo816 } from '../timelines/containers/local_storage/migrate_alert_page_controls'; +import type { StartPlugins } from '../types'; -type LocalStorageMigrator = (storage: Storage) => void; +/* Migrator could be sync or async */ +type LocalStorageMigrator = (storage: Storage, plugins: StartPlugins) => void | Promise; -const runLocalStorageMigration = (fn: LocalStorageMigrator) => { - const storage = new Storage(localStorage); - fn(storage); +const getLocalStorageMigrationRunner = (storage: Storage, plugins: StartPlugins) => { + const runLocalStorageMigration = async (fn: LocalStorageMigrator) => { + await fn(storage, plugins); + }; + return runLocalStorageMigration; }; -export const runDetectionMigrations = () => { - runLocalStorageMigration(migrateAlertPageControlsTo816); +export const runDetectionMigrations = async (storage: Storage, plugins: StartPlugins) => { + const runLocalStorageMigration = getLocalStorageMigrationRunner(storage, plugins); + await runLocalStorageMigration(migrateAlertPageControlsTo816); }; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 17f1ba842f8c..b20e645d71c2 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -256,8 +256,9 @@ export class Plugin implements IPlugin { const subPlugins = await this.createSubPlugins(); + const alerts = await subPlugins.alerts.start(storage, plugins); return { - alerts: subPlugins.alerts.start(storage), + alerts, attackDiscovery: subPlugins.attackDiscovery.start(), cases: subPlugins.cases.start(), cloudDefend: subPlugins.cloudDefend.start(), diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrate_alert_page_contorls.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrate_alert_page_contorls.test.ts index 575d5bcd6dab..de0e085fb4ed 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrate_alert_page_contorls.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrate_alert_page_contorls.test.ts @@ -7,9 +7,10 @@ import { Storage } from '@kbn/kibana-utils-plugin/public'; import { - PAGE_FILTER_STORAGE_KEY, + GET_PAGE_FILTER_STORAGE_KEY, migrateAlertPageControlsTo816, } from './migrate_alert_page_controls'; +import type { StartPlugins } from '../../../types'; const OLD_FORMAT = { viewMode: 'view', @@ -216,40 +217,94 @@ const NEW_FORMAT = { }; const storage = new Storage(localStorage); +const mockPlugins = { + spaces: { + getActiveSpace: jest.fn().mockResolvedValue({ id: 'default' }), + }, +} as unknown as StartPlugins; + describe('migrateAlertPageControlsTo816', () => { beforeEach(() => { storage.clear(); }); - it('should migrate the old format to the new format', () => { - storage.set(PAGE_FILTER_STORAGE_KEY, OLD_FORMAT); - migrateAlertPageControlsTo816(storage); - const migrated = storage.get(PAGE_FILTER_STORAGE_KEY); - expect(migrated).toMatchObject(NEW_FORMAT); - }); + describe('Default space', () => { + beforeEach(() => { + if (mockPlugins.spaces?.getActiveSpace) { + mockPlugins.spaces.getActiveSpace = jest.fn().mockResolvedValue({ id: 'default' }); + } + }); + it('should migrate the old format to the new format', async () => { + storage.set(GET_PAGE_FILTER_STORAGE_KEY(), OLD_FORMAT); + await migrateAlertPageControlsTo816(storage, mockPlugins); + const migrated = storage.get(GET_PAGE_FILTER_STORAGE_KEY()); + expect(migrated).toMatchObject(NEW_FORMAT); + }); - it('should be a no-op if the new format already exists', () => { - storage.set(PAGE_FILTER_STORAGE_KEY, NEW_FORMAT); - migrateAlertPageControlsTo816(storage); - const migrated = storage.get(PAGE_FILTER_STORAGE_KEY); - expect(migrated).toMatchObject(NEW_FORMAT); - }); + it('should be a no-op if the new format already exists', async () => { + storage.set(GET_PAGE_FILTER_STORAGE_KEY(), NEW_FORMAT); + await migrateAlertPageControlsTo816(storage, mockPlugins); + const migrated = storage.get(GET_PAGE_FILTER_STORAGE_KEY()); + expect(migrated).toMatchObject(NEW_FORMAT); + }); - it('should be a no-op if no value is present in localstorage for page filters ', () => { - migrateAlertPageControlsTo816(storage); - const migrated = storage.get(PAGE_FILTER_STORAGE_KEY); - expect(migrated).toBeNull(); + it('should be a no-op if no value is present in localstorage for page filters ', async () => { + await migrateAlertPageControlsTo816(storage, mockPlugins); + const migrated = storage.get(GET_PAGE_FILTER_STORAGE_KEY()); + expect(migrated).toBeNull(); + }); + + it('should convert custom old format correctly', async () => { + const MODIFIED_OLD_FORMAT = structuredClone(OLD_FORMAT); + MODIFIED_OLD_FORMAT.panels['0'].explicitInput.hideExists = true; + MODIFIED_OLD_FORMAT.chainingSystem = 'NONE'; + storage.set(GET_PAGE_FILTER_STORAGE_KEY(), MODIFIED_OLD_FORMAT); + await migrateAlertPageControlsTo816(storage, mockPlugins); + const migrated = storage.get(GET_PAGE_FILTER_STORAGE_KEY()); + const EXPECTED_NEW_FORMAT = structuredClone(NEW_FORMAT); + EXPECTED_NEW_FORMAT.initialChildControlState['0'].hideExists = true; + EXPECTED_NEW_FORMAT.chainingSystem = 'NONE'; + expect(migrated).toMatchObject(EXPECTED_NEW_FORMAT); + }); }); - it('should convert custom old format correctly', () => { - const MODIFIED_OLD_FORMAT = structuredClone(OLD_FORMAT); - MODIFIED_OLD_FORMAT.panels['0'].explicitInput.hideExists = true; - MODIFIED_OLD_FORMAT.chainingSystem = 'NONE'; - storage.set(PAGE_FILTER_STORAGE_KEY, MODIFIED_OLD_FORMAT); - migrateAlertPageControlsTo816(storage); - const migrated = storage.get(PAGE_FILTER_STORAGE_KEY); - const EXPECTED_NEW_FORMAT = structuredClone(NEW_FORMAT); - EXPECTED_NEW_FORMAT.initialChildControlState['0'].hideExists = true; - EXPECTED_NEW_FORMAT.chainingSystem = 'NONE'; - expect(migrated).toMatchObject(EXPECTED_NEW_FORMAT); + describe('Non Default space', () => { + const nonDefaultSpaceId = 'space1'; + beforeEach(() => { + if (mockPlugins.spaces?.getActiveSpace) { + mockPlugins.spaces.getActiveSpace = jest.fn().mockResolvedValue({ id: nonDefaultSpaceId }); + } + }); + it('should migrate the old format to the new format', async () => { + storage.set(GET_PAGE_FILTER_STORAGE_KEY(nonDefaultSpaceId), OLD_FORMAT); + await migrateAlertPageControlsTo816(storage, mockPlugins); + const migrated = storage.get(GET_PAGE_FILTER_STORAGE_KEY(nonDefaultSpaceId)); + expect(migrated).toMatchObject(NEW_FORMAT); + }); + + it('should be a no-op if the new format already exists', async () => { + storage.set(GET_PAGE_FILTER_STORAGE_KEY(nonDefaultSpaceId), NEW_FORMAT); + await migrateAlertPageControlsTo816(storage, mockPlugins); + const migrated = storage.get(GET_PAGE_FILTER_STORAGE_KEY(nonDefaultSpaceId)); + expect(migrated).toMatchObject(NEW_FORMAT); + }); + + it('should be a no-op if no value is present in localstorage for page filters ', async () => { + await migrateAlertPageControlsTo816(storage, mockPlugins); + const migrated = storage.get(GET_PAGE_FILTER_STORAGE_KEY(nonDefaultSpaceId)); + expect(migrated).toBeNull(); + }); + + it('should convert custom old format correctly', async () => { + const MODIFIED_OLD_FORMAT = structuredClone(OLD_FORMAT); + MODIFIED_OLD_FORMAT.panels['0'].explicitInput.hideExists = true; + MODIFIED_OLD_FORMAT.chainingSystem = 'NONE'; + storage.set(GET_PAGE_FILTER_STORAGE_KEY(nonDefaultSpaceId), MODIFIED_OLD_FORMAT); + await migrateAlertPageControlsTo816(storage, mockPlugins); + const migrated = storage.get(GET_PAGE_FILTER_STORAGE_KEY(nonDefaultSpaceId)); + const EXPECTED_NEW_FORMAT = structuredClone(NEW_FORMAT); + EXPECTED_NEW_FORMAT.initialChildControlState['0'].hideExists = true; + EXPECTED_NEW_FORMAT.chainingSystem = 'NONE'; + expect(migrated).toMatchObject(EXPECTED_NEW_FORMAT); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrate_alert_page_controls.ts b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrate_alert_page_controls.ts index ac46d4203515..7152ef7738bc 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrate_alert_page_controls.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrate_alert_page_controls.ts @@ -7,8 +7,10 @@ import type { DefaultControlState, ControlGroupRuntimeState } from '@kbn/controls-plugin/common'; import type { Storage } from '@kbn/kibana-utils-plugin/public'; +import type { StartPlugins } from '../../../types'; -export const PAGE_FILTER_STORAGE_KEY = 'siem.default.pageFilters'; +export const GET_PAGE_FILTER_STORAGE_KEY = (spaceId: string = 'default') => + `siem.${spaceId}.pageFilters`; interface OldFormat { viewMode: string; @@ -96,8 +98,11 @@ interface NewFormatExplicitInput { * This migration script is to migrate the old format to the new format. * */ -export function migrateAlertPageControlsTo816(storage: Storage) { - const oldFormat: OldFormat = storage.get(PAGE_FILTER_STORAGE_KEY); +export async function migrateAlertPageControlsTo816(storage: Storage, plugins: StartPlugins) { + const space = await plugins.spaces?.getActiveSpace(); + const spaceId = space?.id ?? 'default'; + const storageKey = GET_PAGE_FILTER_STORAGE_KEY(spaceId); + const oldFormat: OldFormat = storage.get(GET_PAGE_FILTER_STORAGE_KEY(spaceId)); if (oldFormat && Object.keys(oldFormat).includes('panels')) { // Only run when it is old format const newFormat: ControlGroupRuntimeState = { @@ -131,6 +136,6 @@ export function migrateAlertPageControlsTo816(storage: Storage) { }; } - storage.set(PAGE_FILTER_STORAGE_KEY, newFormat); + storage.set(storageKey, newFormat); } } diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 55fce6a46dba..6642e02d5ecd 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -248,7 +248,7 @@ export interface SubPlugins { // TODO: find a better way to defined these types export interface StartedSubPlugins { [CASES_SUB_PLUGIN_KEY]: ReturnType; - alerts: ReturnType; + alerts: Awaited>; attackDiscovery: ReturnType; cloudDefend: ReturnType; cloudSecurityPosture: ReturnType;