[Maps] Use SO-references for geo-containment alerts (#114559)

This commit is contained in:
Thomas Neirynck 2021-10-15 13:51:47 -04:00 committed by GitHub
parent 411886ac8b
commit 712fac6042
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 428 additions and 27 deletions

View file

@ -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',
},
],
});
});
});

View file

@ -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<GeoContainmentParams, 'indexId' | 'boundaryIndexId'> & {
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<RawAlert>
): SavedObjectUnsanitizedDoc<RawAlert> {
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],
};
}

View file

@ -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', () => {

View file

@ -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
)
);

View file

@ -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<string, unknown>;
shapesIdsNamesMap: Record<string, unknown>;
@ -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<GeoContainmentExtractedParams> => {
return extractEntityAndBoundaryReferences(params);
},
injectReferences: (
params: GeoContainmentExtractedParams,
references: SavedObjectReference[]
) => {
return injectEntityAndBoundaryIds(params, references);
},
},
};
}

View file

@ -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

View file

@ -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,

View file

@ -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',
},
],
});
});
});

View file

@ -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) => {