diff --git a/x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.test.ts new file mode 100644 index 0000000000000..779e201635495 --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { extractEntityAndBoundaryReferences } from './migrations'; + +describe('geo_containment migration utilities', () => { + test('extractEntityAndBoundaryReferences', () => { + expect( + extractEntityAndBoundaryReferences({ + index: 'foo*', + indexId: 'foobar', + geoField: 'geometry', + entity: 'vehicle_id', + dateField: '@timestamp', + boundaryType: 'entireIndex', + boundaryIndexTitle: 'boundary*', + boundaryIndexId: 'boundaryid', + boundaryGeoField: 'geometry', + }) + ).toEqual({ + params: { + boundaryGeoField: 'geometry', + boundaryIndexRefName: 'boundary_index_boundaryid', + boundaryIndexTitle: 'boundary*', + boundaryType: 'entireIndex', + dateField: '@timestamp', + entity: 'vehicle_id', + geoField: 'geometry', + index: 'foo*', + indexRefName: 'tracked_index_foobar', + }, + references: [ + { + id: 'foobar', + name: 'param:tracked_index_foobar', + type: 'index-pattern', + }, + { + id: 'boundaryid', + name: 'param:boundary_index_boundaryid', + type: 'index-pattern', + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.ts new file mode 100644 index 0000000000000..113b4cf796d2f --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.ts @@ -0,0 +1,93 @@ +/* + * 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 { + SavedObjectAttributes, + SavedObjectReference, + SavedObjectUnsanitizedDoc, +} from 'kibana/server'; +import { AlertTypeParams } from '../../index'; +import { Query } from '../../../../../../src/plugins/data/common/query'; +import { RawAlert } from '../../types'; + +// These definitions are dupes of the SO-types in stack_alerts/geo_containment +// There are not exported to avoid deep imports from stack_alerts plugins into here +const GEO_CONTAINMENT_ID = '.geo-containment'; +interface GeoContainmentParams extends AlertTypeParams { + index: string; + indexId: string; + geoField: string; + entity: string; + dateField: string; + boundaryType: string; + boundaryIndexTitle: string; + boundaryIndexId: string; + boundaryGeoField: string; + boundaryNameField?: string; + indexQuery?: Query; + boundaryIndexQuery?: Query; +} +type GeoContainmentExtractedParams = Omit & { + indexRefName: string; + boundaryIndexRefName: string; +}; + +export function extractEntityAndBoundaryReferences(params: GeoContainmentParams): { + params: GeoContainmentExtractedParams; + references: SavedObjectReference[]; +} { + const { indexId, boundaryIndexId, ...otherParams } = params; + + const indexRefNamePrefix = 'tracked_index_'; + const boundaryRefNamePrefix = 'boundary_index_'; + + // Since these are stack-alerts, we need to prefix with the `param:`-namespace + const references = [ + { + name: `param:${indexRefNamePrefix}${indexId}`, + type: `index-pattern`, + id: indexId as string, + }, + { + name: `param:${boundaryRefNamePrefix}${boundaryIndexId}`, + type: 'index-pattern', + id: boundaryIndexId as string, + }, + ]; + return { + params: { + ...otherParams, + indexRefName: `${indexRefNamePrefix}${indexId}`, + boundaryIndexRefName: `${boundaryRefNamePrefix}${boundaryIndexId}`, + }, + references, + }; +} + +export function extractRefsFromGeoContainmentAlert( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + if (doc.attributes.alertTypeId !== GEO_CONTAINMENT_ID) { + return doc; + } + + const { + attributes: { params }, + } = doc; + + const { params: newParams, references } = extractEntityAndBoundaryReferences( + params as GeoContainmentParams + ); + return { + ...doc, + attributes: { + ...doc.attributes, + params: newParams as SavedObjectAttributes, + }, + references: [...(doc.references || []), ...references], + }; +} diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts index 3f7cdecf4affd..3822334579137 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -1913,6 +1913,96 @@ describe('successful migrations', () => { ], }); }); + + test('geo-containment alert migration extracts boundary and index references', () => { + const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const alert = { + ...getMockData({ + alertTypeId: '.geo-containment', + params: { + indexId: 'foo', + boundaryIndexId: 'bar', + }, + }), + }; + + const migratedAlert = migration7160(alert, migrationContext); + + expect(migratedAlert.references).toEqual([ + { id: 'foo', name: 'param:tracked_index_foo', type: 'index-pattern' }, + { id: 'bar', name: 'param:boundary_index_bar', type: 'index-pattern' }, + ]); + + expect(migratedAlert.attributes.params).toEqual({ + boundaryIndexRefName: 'boundary_index_bar', + indexRefName: 'tracked_index_foo', + }); + + expect(migratedAlert.attributes.params.indexId).toEqual(undefined); + expect(migratedAlert.attributes.params.boundaryIndexId).toEqual(undefined); + }); + + test('geo-containment alert migration should preserve foreign references', () => { + const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const alert = { + ...getMockData({ + alertTypeId: '.geo-containment', + params: { + indexId: 'foo', + boundaryIndexId: 'bar', + }, + }), + references: [ + { + name: 'foreign-name', + id: '999', + type: 'foreign-name', + }, + ], + }; + + const migratedAlert = migration7160(alert, migrationContext); + + expect(migratedAlert.references).toEqual([ + { + name: 'foreign-name', + id: '999', + type: 'foreign-name', + }, + { id: 'foo', name: 'param:tracked_index_foo', type: 'index-pattern' }, + { id: 'bar', name: 'param:boundary_index_bar', type: 'index-pattern' }, + ]); + + expect(migratedAlert.attributes.params).toEqual({ + boundaryIndexRefName: 'boundary_index_bar', + indexRefName: 'tracked_index_foo', + }); + + expect(migratedAlert.attributes.params.indexId).toEqual(undefined); + expect(migratedAlert.attributes.params.boundaryIndexId).toEqual(undefined); + }); + + test('geo-containment alert migration ignores other alert-types', () => { + const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const alert = { + ...getMockData({ + alertTypeId: '.foo', + references: [ + { + name: 'foreign-name', + id: '999', + type: 'foreign-name', + }, + ], + }), + }; + + const migratedAlert = migration7160(alert, migrationContext); + + expect(typeof migratedAlert.attributes.legacyId).toEqual('string'); // introduced by setLegacyId migration + delete migratedAlert.attributes.legacyId; + expect(migratedAlert).toEqual(alert); + }); }); describe('8.0.0', () => { diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index 9dcca54285279..0a1d7bfc8a9d7 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -19,6 +19,7 @@ import { import { RawAlert, RawAlertAction } from '../types'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; import type { IsMigrationNeededPredicate } from '../../../encrypted_saved_objects/server'; +import { extractRefsFromGeoContainmentAlert } from './geo_containment/migrations'; const SIEM_APP_ID = 'securitySolution'; const SIEM_SERVER_APP_ID = 'siem'; @@ -117,7 +118,8 @@ export function getMigrations( pipeMigrations( setLegacyId, getRemovePreconfiguredConnectorsFromReferencesFn(isPreconfigured), - addRuleIdsToLegacyNotificationReferences + addRuleIdsToLegacyNotificationReferences, + extractRefsFromGeoContainmentAlert ) ); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts index 111fda3bdaca8..2a98a4670f2b5 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; -import { Logger } from 'src/core/server'; +import { Logger, SavedObjectReference } from 'src/core/server'; import { STACK_ALERTS_FEATURE_ID } from '../../../common'; import { getGeoContainmentExecutor } from './geo_containment'; import { @@ -15,14 +15,37 @@ import { AlertTypeState, AlertInstanceState, AlertInstanceContext, + RuleParamsAndRefs, AlertTypeParams, } from '../../../../alerting/server'; import { Query } from '../../../../../../src/plugins/data/common/query'; -export const GEO_CONTAINMENT_ID = '.geo-containment'; export const ActionGroupId = 'Tracked entity contained'; export const RecoveryActionGroupId = 'notGeoContained'; +export const GEO_CONTAINMENT_ID = '.geo-containment'; +export interface GeoContainmentParams extends AlertTypeParams { + index: string; + indexId: string; + geoField: string; + entity: string; + dateField: string; + boundaryType: string; + boundaryIndexTitle: string; + boundaryIndexId: string; + boundaryGeoField: string; + boundaryNameField?: string; + indexQuery?: Query; + boundaryIndexQuery?: Query; +} +export type GeoContainmentExtractedParams = Omit< + GeoContainmentParams, + 'indexId' | 'boundaryIndexId' +> & { + indexRefName: string; + boundaryIndexRefName: string; +}; + const actionVariableContextEntityIdLabel = i18n.translate( 'xpack.stackAlerts.geoContainment.actionVariableContextEntityIdLabel', { @@ -103,20 +126,6 @@ export const ParamsSchema = schema.object({ boundaryIndexQuery: schema.maybe(schema.any({})), }); -export interface GeoContainmentParams extends AlertTypeParams { - index: string; - indexId: string; - geoField: string; - entity: string; - dateField: string; - boundaryType: string; - boundaryIndexTitle: string; - boundaryIndexId: string; - boundaryGeoField: string; - boundaryNameField?: string; - indexQuery?: Query; - boundaryIndexQuery?: Query; -} export interface GeoContainmentState extends AlertTypeState { shapesFilters: Record; shapesIdsNamesMap: Record; @@ -140,7 +149,7 @@ export interface GeoContainmentInstanceContext extends AlertInstanceContext { export type GeoContainmentAlertType = AlertType< GeoContainmentParams, - never, // Only use if defining useSavedObjectReferences hook + GeoContainmentExtractedParams, GeoContainmentState, GeoContainmentInstanceState, GeoContainmentInstanceContext, @@ -148,6 +157,56 @@ export type GeoContainmentAlertType = AlertType< typeof RecoveryActionGroupId >; +export function extractEntityAndBoundaryReferences(params: GeoContainmentParams): { + params: GeoContainmentExtractedParams; + references: SavedObjectReference[]; +} { + const { indexId, boundaryIndexId, ...otherParams } = params; + + // Reference names omit the `param:`-prefix. This is handled by the alerting framework already + const references = [ + { + name: `tracked_index_${indexId}`, + type: 'index-pattern', + id: indexId as string, + }, + { + name: `boundary_index_${boundaryIndexId}`, + type: 'index-pattern', + id: boundaryIndexId as string, + }, + ]; + return { + params: { + ...otherParams, + indexRefName: `tracked_index_${indexId}`, + boundaryIndexRefName: `boundary_index_${boundaryIndexId}`, + }, + references, + }; +} + +export function injectEntityAndBoundaryIds( + params: GeoContainmentExtractedParams, + references: SavedObjectReference[] +): GeoContainmentParams { + const { indexRefName, boundaryIndexRefName, ...otherParams } = params; + const { id: indexId = null } = references.find((ref) => ref.name === indexRefName) || {}; + const { id: boundaryIndexId = null } = + references.find((ref) => ref.name === boundaryIndexRefName) || {}; + if (!indexId) { + throw new Error(`Index "${indexId}" not found in references array`); + } + if (!boundaryIndexId) { + throw new Error(`Boundary index "${boundaryIndexId}" not found in references array`); + } + return { + ...otherParams, + indexId, + boundaryIndexId, + } as GeoContainmentParams; +} + export function getAlertType(logger: Logger): GeoContainmentAlertType { const alertTypeName = i18n.translate('xpack.stackAlerts.geoContainment.alertTypeTitle', { defaultMessage: 'Tracking containment', @@ -179,5 +238,18 @@ export function getAlertType(logger: Logger): GeoContainmentAlertType { actionVariables, minimumLicenseRequired: 'gold', isExportable: true, + useSavedObjectReferences: { + extractReferences: ( + params: GeoContainmentParams + ): RuleParamsAndRefs => { + return extractEntityAndBoundaryReferences(params); + }, + injectReferences: ( + params: GeoContainmentExtractedParams, + references: SavedObjectReference[] + ) => { + return injectEntityAndBoundaryIds(params, references); + }, + }, }; } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts index 21a536dd474ba..f227ae4fc23cc 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts @@ -12,13 +12,14 @@ import { executeEsQueryFactory, getShapesFilters, OTHER_CATEGORY } from './es_qu import { AlertServices } from '../../../../alerting/server'; import { ActionGroupId, - GEO_CONTAINMENT_ID, GeoContainmentInstanceState, GeoContainmentAlertType, GeoContainmentInstanceContext, GeoContainmentState, } from './alert_type'; +import { GEO_CONTAINMENT_ID } from './alert_type'; + export type LatestEntityLocation = GeoContainmentInstanceState; // Flatten agg results and get latest locations for each entity diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts index 023ea168a77d2..195ffb97bd81f 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts @@ -8,7 +8,6 @@ import { Logger } from 'src/core/server'; import { AlertingSetup } from '../../types'; import { - GeoContainmentParams, GeoContainmentState, GeoContainmentInstanceState, GeoContainmentInstanceContext, @@ -17,6 +16,8 @@ import { RecoveryActionGroupId, } from './alert_type'; +import { GeoContainmentExtractedParams, GeoContainmentParams } from './alert_type'; + interface RegisterParams { logger: Logger; alerting: AlertingSetup; @@ -26,7 +27,7 @@ export function register(params: RegisterParams) { const { logger, alerting } = params; alerting.registerType< GeoContainmentParams, - never, // Only use if defining useSavedObjectReferences hook + GeoContainmentExtractedParams, GeoContainmentState, GeoContainmentInstanceState, GeoContainmentInstanceContext, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts index e8f699eb06161..9fc382240d0be 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts @@ -6,7 +6,12 @@ */ import { loggingSystemMock } from '../../../../../../../src/core/server/mocks'; -import { getAlertType, GeoContainmentParams } from '../alert_type'; +import { + getAlertType, + injectEntityAndBoundaryIds, + GeoContainmentParams, + extractEntityAndBoundaryReferences, +} from '../alert_type'; describe('alertType', () => { const logger = loggingSystemMock.create().get(); @@ -43,4 +48,94 @@ describe('alertType', () => { expect(alertType.validate?.params?.validate(params)).toBeTruthy(); }); + + test('injectEntityAndBoundaryIds', () => { + expect( + injectEntityAndBoundaryIds( + { + boundaryGeoField: 'geometry', + boundaryIndexRefName: 'boundary_index_boundaryid', + boundaryIndexTitle: 'boundary*', + boundaryType: 'entireIndex', + dateField: '@timestamp', + entity: 'vehicle_id', + geoField: 'geometry', + index: 'foo*', + indexRefName: 'tracked_index_foobar', + }, + [ + { + id: 'foreign', + name: 'foobar', + type: 'foreign', + }, + { + id: 'foobar', + name: 'tracked_index_foobar', + type: 'index-pattern', + }, + { + id: 'foreignToo', + name: 'boundary_index_shouldbeignored', + type: 'index-pattern', + }, + { + id: 'boundaryid', + name: 'boundary_index_boundaryid', + type: 'index-pattern', + }, + ] + ) + ).toEqual({ + index: 'foo*', + indexId: 'foobar', + geoField: 'geometry', + entity: 'vehicle_id', + dateField: '@timestamp', + boundaryType: 'entireIndex', + boundaryIndexTitle: 'boundary*', + boundaryIndexId: 'boundaryid', + boundaryGeoField: 'geometry', + }); + }); + + test('extractEntityAndBoundaryReferences', () => { + expect( + extractEntityAndBoundaryReferences({ + index: 'foo*', + indexId: 'foobar', + geoField: 'geometry', + entity: 'vehicle_id', + dateField: '@timestamp', + boundaryType: 'entireIndex', + boundaryIndexTitle: 'boundary*', + boundaryIndexId: 'boundaryid', + boundaryGeoField: 'geometry', + }) + ).toEqual({ + params: { + boundaryGeoField: 'geometry', + boundaryIndexRefName: 'boundary_index_boundaryid', + boundaryIndexTitle: 'boundary*', + boundaryType: 'entireIndex', + dateField: '@timestamp', + entity: 'vehicle_id', + geoField: 'geometry', + index: 'foo*', + indexRefName: 'tracked_index_foobar', + }, + references: [ + { + id: 'foobar', + name: 'tracked_index_foobar', + type: 'index-pattern', + }, + { + id: 'boundaryid', + name: 'boundary_index_boundaryid', + type: 'index-pattern', + }, + ], + }); + }); }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts index 364c484a02080..8b78441d174b2 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts @@ -17,11 +17,8 @@ import { getGeoContainmentExecutor, } from '../geo_containment'; import { OTHER_CATEGORY } from '../es_query_builder'; -import { - GeoContainmentInstanceContext, - GeoContainmentInstanceState, - GeoContainmentParams, -} from '../alert_type'; +import { GeoContainmentInstanceContext, GeoContainmentInstanceState } from '../alert_type'; +import type { GeoContainmentParams } from '../alert_type'; const alertInstanceFactory = (contextKeys: unknown[], testAlertActionArr: unknown[]) => (instanceId: string) => {