Skip to content

Commit

Permalink
[Security Solution] Migration of Alert Page controls for non-default …
Browse files Browse the repository at this point in the history
…Spaces. (elastic#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.
  • Loading branch information
logeekal authored and CAWilson94 committed Nov 18, 2024
1 parent 4ea2b1d commit 440fee7
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 43 deletions.
5 changes: 3 additions & 2 deletions x-pack/plugins/security_solution/public/detections/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<SecuritySubPlugin> {
await runDetectionMigrations(storage, plugins);

return {
storageDataTables: {
Expand Down
19 changes: 12 additions & 7 deletions x-pack/plugins/security_solution/public/detections/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;

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);
};
3 changes: 2 additions & 1 deletion x-pack/plugins/security_solution/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,9 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
plugins: StartPlugins
): Promise<StartedSubPlugins> {
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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<NewFormatExplicitInput & DefaultControlState> = {
Expand Down Expand Up @@ -131,6 +136,6 @@ export function migrateAlertPageControlsTo816(storage: Storage) {
};
}

storage.set(PAGE_FILTER_STORAGE_KEY, newFormat);
storage.set(storageKey, newFormat);
}
}
2 changes: 1 addition & 1 deletion x-pack/plugins/security_solution/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export interface SubPlugins {
// TODO: find a better way to defined these types
export interface StartedSubPlugins {
[CASES_SUB_PLUGIN_KEY]: ReturnType<Cases['start']>;
alerts: ReturnType<Detections['start']>;
alerts: Awaited<ReturnType<Detections['start']>>;
attackDiscovery: ReturnType<AttackDiscovery['start']>;
cloudDefend: ReturnType<CloudDefend['start']>;
cloudSecurityPosture: ReturnType<CloudSecurityPosture['start']>;
Expand Down

0 comments on commit 440fee7

Please sign in to comment.