[SIEM][Detection Engine] Refactors signal rule alert type into smaller code by creating functions (#60294)

Refactors signal rule alert type into a smaller executor

## Summary

* Breaks out the schema into its own file and function
* Breaks out the action group into its own file and function
* Moves misc types being added to this into the `./types` file
* Breaks out all the writing of errors and success into their own functions
* Uses destructuring to pull data out of some of the data types
* Tweaks the gap detection to accept a date instead of moment to ease "ergonomics"   
* Updates unit tests for the gap detection

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios
This commit is contained in:
Frank Hassanabad 2020-03-16 15:05:28 -06:00 committed by GitHub
parent 83a5b78fa9
commit ddedf23149
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 417 additions and 239 deletions

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectsFindResponse, SavedObject } from 'src/core/server';
import { AlertServices } from '../../../../../../../plugins/alerting/server';
import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types';
import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings';
interface CurrentStatusSavedObjectParams {
alertId: string;
services: AlertServices;
ruleStatusSavedObjects: SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes>;
}
export const getCurrentStatusSavedObject = async ({
alertId,
services,
ruleStatusSavedObjects,
}: CurrentStatusSavedObjectParams): Promise<SavedObject<
IRuleSavedAttributesSavedObjectAttributes
>> => {
if (ruleStatusSavedObjects.saved_objects.length === 0) {
// create
const date = new Date().toISOString();
const currentStatusSavedObject = await services.savedObjectsClient.create<
IRuleSavedAttributesSavedObjectAttributes
>(ruleStatusSavedObjectType, {
alertId, // do a search for this id.
statusDate: date,
status: 'going to run',
lastFailureAt: null,
lastSuccessAt: null,
lastFailureMessage: null,
lastSuccessMessage: null,
});
return currentStatusSavedObject;
} else {
// update 0th to executing.
const currentStatusSavedObject = ruleStatusSavedObjects.saved_objects[0];
const sDate = new Date().toISOString();
currentStatusSavedObject.attributes.status = 'going to run';
currentStatusSavedObject.attributes.statusDate = sDate;
await services.savedObjectsClient.update(
ruleStatusSavedObjectType,
currentStatusSavedObject.id,
{
...currentStatusSavedObject.attributes,
}
);
return currentStatusSavedObject;
}
};

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectsFindResponse } from 'kibana/server';
import { AlertServices } from '../../../../../../../plugins/alerting/server';
import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings';
import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types';
interface GetRuleStatusSavedObject {
alertId: string;
services: AlertServices;
}
export const getRuleStatusSavedObjects = async ({
alertId,
services,
}: GetRuleStatusSavedObject): Promise<SavedObjectsFindResponse<
IRuleSavedAttributesSavedObjectAttributes
>> => {
return services.savedObjectsClient.find<IRuleSavedAttributesSavedObjectAttributes>({
type: ruleStatusSavedObjectType,
perPage: 6, // 0th element is current status, 1-5 is last 5 failures.
sortField: 'statusDate',
sortOrder: 'desc',
search: `${alertId}`,
searchFields: ['alertId'],
});
};

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const siemRuleActionGroups = [
{
id: 'default',
name: i18n.translate('xpack.siem.detectionEngine.signalRuleAlert.actionGroups.default', {
defaultMessage: 'Default',
}),
},
];

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants';
/**
* This is the schema for the Alert Rule that represents the SIEM alert for signals
* that index into the .siem-signals-${space-id}
*/
export const signalParamsSchema = () =>
schema.object({
description: schema.string(),
note: schema.nullable(schema.string()),
falsePositives: schema.arrayOf(schema.string(), { defaultValue: [] }),
from: schema.string(),
ruleId: schema.string(),
immutable: schema.boolean({ defaultValue: false }),
index: schema.nullable(schema.arrayOf(schema.string())),
language: schema.nullable(schema.string()),
outputIndex: schema.nullable(schema.string()),
savedId: schema.nullable(schema.string()),
timelineId: schema.nullable(schema.string()),
timelineTitle: schema.nullable(schema.string()),
meta: schema.nullable(schema.object({}, { allowUnknowns: true })),
query: schema.nullable(schema.string()),
filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))),
maxSignals: schema.number({ defaultValue: DEFAULT_MAX_SIGNALS }),
riskScore: schema.number(),
severity: schema.string(),
threat: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))),
to: schema.string(),
type: schema.string(),
references: schema.arrayOf(schema.string(), { defaultValue: [] }),
version: schema.number({ defaultValue: 1 }),
});

View file

@ -4,35 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { Logger } from 'src/core/server';
import moment from 'moment';
import { i18n } from '@kbn/i18n';
import {
SIGNALS_ID,
DEFAULT_MAX_SIGNALS,
DEFAULT_SEARCH_AFTER_PAGE_SIZE,
} from '../../../../common/constants';
import { SIGNALS_ID, DEFAULT_SEARCH_AFTER_PAGE_SIZE } from '../../../../common/constants';
import { buildEventsSearchQuery } from './build_events_query';
import { getInputIndex } from './get_input_output_index';
import { searchAfterAndBulkCreate } from './search_after_bulk_create';
import { getFilter } from './get_filter';
import { SignalRuleAlertTypeDefinition } from './types';
import { SignalRuleAlertTypeDefinition, AlertAttributes } from './types';
import { getGapBetweenRuns } from './utils';
import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings';
import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types';
interface AlertAttributes {
enabled: boolean;
name: string;
tags: string[];
createdBy: string;
createdAt: string;
updatedBy: string;
schedule: {
interval: string;
};
}
import { writeSignalRuleExceptionToSavedObject } from './write_signal_rule_exception_to_saved_object';
import { signalParamsSchema } from './signal_params_schema';
import { siemRuleActionGroups } from './siem_rule_action_groups';
import { writeGapErrorToSavedObject } from './write_gap_error_to_saved_object';
import { getRuleStatusSavedObjects } from './get_rule_status_saved_objects';
import { getCurrentStatusSavedObject } from './get_current_status_saved_object';
import { writeCurrentStatusSucceeded } from './write_current_status_succeeded';
export const signalRulesAlertType = ({
logger,
version,
@ -43,43 +31,11 @@ export const signalRulesAlertType = ({
return {
id: SIGNALS_ID,
name: 'SIEM Signals',
actionGroups: [
{
id: 'default',
name: i18n.translate('xpack.siem.detectionEngine.signalRuleAlert.actionGroups.default', {
defaultMessage: 'Default',
}),
},
],
actionGroups: siemRuleActionGroups,
defaultActionGroupId: 'default',
validate: {
params: schema.object({
description: schema.string(),
note: schema.nullable(schema.string()),
falsePositives: schema.arrayOf(schema.string(), { defaultValue: [] }),
from: schema.string(),
ruleId: schema.string(),
immutable: schema.boolean({ defaultValue: false }),
index: schema.nullable(schema.arrayOf(schema.string())),
language: schema.nullable(schema.string()),
outputIndex: schema.nullable(schema.string()),
savedId: schema.nullable(schema.string()),
timelineId: schema.nullable(schema.string()),
timelineTitle: schema.nullable(schema.string()),
meta: schema.nullable(schema.object({}, { allowUnknowns: true })),
query: schema.nullable(schema.string()),
filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))),
maxSignals: schema.number({ defaultValue: DEFAULT_MAX_SIGNALS }),
riskScore: schema.number(),
severity: schema.string(),
threat: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))),
to: schema.string(),
type: schema.string(),
references: schema.arrayOf(schema.string(), { defaultValue: [] }),
version: schema.number({ defaultValue: 1 }),
}),
params: signalParamsSchema(),
},
// fun fact: previousStartedAt is not actually a Date but a String of a date
async executor({ previousStartedAt, alertId, services, params }) {
const {
from,
@ -93,89 +49,43 @@ export const signalRulesAlertType = ({
to,
type,
} = params;
// TODO: Remove this hard extraction of name once this is fixed: https://github.com/elastic/kibana/issues/50522
const savedObject = await services.savedObjectsClient.get<AlertAttributes>('alert', alertId);
const ruleStatusSavedObjects = await services.savedObjectsClient.find<
IRuleSavedAttributesSavedObjectAttributes
>({
type: ruleStatusSavedObjectType,
perPage: 6, // 0th element is current status, 1-5 is last 5 failures.
sortField: 'statusDate',
sortOrder: 'desc',
search: `${alertId}`,
searchFields: ['alertId'],
const ruleStatusSavedObjects = await getRuleStatusSavedObjects({
alertId,
services,
});
let currentStatusSavedObject;
if (ruleStatusSavedObjects.saved_objects.length === 0) {
// create
const date = new Date().toISOString();
currentStatusSavedObject = await services.savedObjectsClient.create<
IRuleSavedAttributesSavedObjectAttributes
>(ruleStatusSavedObjectType, {
alertId, // do a search for this id.
statusDate: date,
status: 'going to run',
lastFailureAt: null,
lastSuccessAt: null,
lastFailureMessage: null,
lastSuccessMessage: null,
});
} else {
// update 0th to executing.
currentStatusSavedObject = ruleStatusSavedObjects.saved_objects[0];
const sDate = new Date().toISOString();
currentStatusSavedObject.attributes.status = 'going to run';
currentStatusSavedObject.attributes.statusDate = sDate;
await services.savedObjectsClient.update(
ruleStatusSavedObjectType,
currentStatusSavedObject.id,
{
...currentStatusSavedObject.attributes,
}
);
}
const name = savedObject.attributes.name;
const tags = savedObject.attributes.tags;
const currentStatusSavedObject = await getCurrentStatusSavedObject({
alertId,
services,
ruleStatusSavedObjects,
});
const {
name,
tags,
createdAt,
createdBy,
updatedBy,
enabled,
schedule: { interval },
} = savedObject.attributes;
const createdBy = savedObject.attributes.createdBy;
const createdAt = savedObject.attributes.createdAt;
const updatedBy = savedObject.attributes.updatedBy;
const updatedAt = savedObject.updated_at ?? '';
const interval = savedObject.attributes.schedule.interval;
const enabled = savedObject.attributes.enabled;
const gap = getGapBetweenRuns({
previousStartedAt: previousStartedAt != null ? moment(previousStartedAt) : null, // TODO: Remove this once previousStartedAt is no longer a string
interval,
from,
to,
});
if (gap != null && gap.asMilliseconds() > 0) {
logger.warn(
`Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.`
);
// write a failure status whenever we have a time gap
// this is a temporary solution until general activity
// monitoring is developed as a feature
const gapDate = new Date().toISOString();
await services.savedObjectsClient.create(ruleStatusSavedObjectType, {
alertId,
statusDate: gapDate,
status: 'failed',
lastFailureAt: gapDate,
lastSuccessAt: currentStatusSavedObject.attributes.lastSuccessAt,
lastFailureMessage: `Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.`,
lastSuccessMessage: currentStatusSavedObject.attributes.lastSuccessMessage,
});
if (ruleStatusSavedObjects.saved_objects.length >= 6) {
// delete fifth status and prepare to insert a newer one.
const toDelete = ruleStatusSavedObjects.saved_objects.slice(5);
await toDelete.forEach(async item =>
services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id)
);
}
}
const gap = getGapBetweenRuns({ previousStartedAt, interval, from, to });
await writeGapErrorToSavedObject({
alertId,
logger,
ruleId: ruleId ?? '(unknown rule id)',
currentStatusSavedObject,
services,
gap,
ruleStatusSavedObjects,
name,
});
// set searchAfter page size to be the lesser of default page size or maxSignals.
const searchAfterSize =
DEFAULT_SEARCH_AFTER_PAGE_SIZE <= params.maxSignals
@ -243,107 +153,45 @@ export const signalRulesAlertType = ({
logger.debug(
`Finished signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}"`
);
const sDate = new Date().toISOString();
currentStatusSavedObject.attributes.status = 'succeeded';
currentStatusSavedObject.attributes.statusDate = sDate;
currentStatusSavedObject.attributes.lastSuccessAt = sDate;
currentStatusSavedObject.attributes.lastSuccessMessage = 'succeeded';
await services.savedObjectsClient.update(
ruleStatusSavedObjectType,
currentStatusSavedObject.id,
{
...currentStatusSavedObject.attributes,
}
);
} else {
logger.error(
`Error processing signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}"`
);
const sDate = new Date().toISOString();
currentStatusSavedObject.attributes.status = 'failed';
currentStatusSavedObject.attributes.statusDate = sDate;
currentStatusSavedObject.attributes.lastFailureAt = sDate;
currentStatusSavedObject.attributes.lastFailureMessage = `Bulk Indexing signals failed. Check logs for further details \nRule name: "${name}"\nid: "${alertId}"\nrule_id: "${ruleId}"\n`;
// current status is failing
await services.savedObjectsClient.update(
ruleStatusSavedObjectType,
currentStatusSavedObject.id,
{
...currentStatusSavedObject.attributes,
}
);
// create new status for historical purposes
await services.savedObjectsClient.create(ruleStatusSavedObjectType, {
...currentStatusSavedObject.attributes,
await writeCurrentStatusSucceeded({
services,
currentStatusSavedObject,
});
} else {
await writeSignalRuleExceptionToSavedObject({
name,
alertId,
currentStatusSavedObject,
logger,
message: `Bulk Indexing signals failed. Check logs for further details \nRule name: "${name}"\nid: "${alertId}"\nrule_id: "${ruleId}"\n`,
services,
ruleStatusSavedObjects,
ruleId: ruleId ?? '(unknown rule id)',
});
if (ruleStatusSavedObjects.saved_objects.length >= 6) {
// delete fifth status and prepare to insert a newer one.
const toDelete = ruleStatusSavedObjects.saved_objects.slice(5);
await toDelete.forEach(async item =>
services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id)
);
}
}
} catch (err) {
logger.error(
`Error from signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}", ${err.message}`
);
const sDate = new Date().toISOString();
currentStatusSavedObject.attributes.status = 'failed';
currentStatusSavedObject.attributes.statusDate = sDate;
currentStatusSavedObject.attributes.lastFailureAt = sDate;
currentStatusSavedObject.attributes.lastFailureMessage = err.message;
// current status is failing
await services.savedObjectsClient.update(
ruleStatusSavedObjectType,
currentStatusSavedObject.id,
{
...currentStatusSavedObject.attributes,
}
);
// create new status for historical purposes
await services.savedObjectsClient.create(ruleStatusSavedObjectType, {
...currentStatusSavedObject.attributes,
await writeSignalRuleExceptionToSavedObject({
name,
alertId,
currentStatusSavedObject,
logger,
message: err?.message ?? '(no error message given)',
services,
ruleStatusSavedObjects,
ruleId: ruleId ?? '(unknown rule id)',
});
if (ruleStatusSavedObjects.saved_objects.length >= 6) {
// delete fifth status and prepare to insert a newer one.
const toDelete = ruleStatusSavedObjects.saved_objects.slice(5);
await toDelete.forEach(async item =>
services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id)
);
}
}
} catch (exception) {
logger.error(
`Error from signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" message: ${exception.message}`
);
const sDate = new Date().toISOString();
currentStatusSavedObject.attributes.status = 'failed';
currentStatusSavedObject.attributes.statusDate = sDate;
currentStatusSavedObject.attributes.lastFailureAt = sDate;
currentStatusSavedObject.attributes.lastFailureMessage = exception.message;
// current status is failing
await services.savedObjectsClient.update(
ruleStatusSavedObjectType,
currentStatusSavedObject.id,
{
...currentStatusSavedObject.attributes,
}
);
// create new status for historical purposes
await services.savedObjectsClient.create(ruleStatusSavedObjectType, {
...currentStatusSavedObject.attributes,
await writeSignalRuleExceptionToSavedObject({
name,
alertId,
currentStatusSavedObject,
logger,
message: exception?.message ?? '(no error message given)',
services,
ruleStatusSavedObjects,
ruleId: ruleId ?? '(unknown rule id)',
});
if (ruleStatusSavedObjects.saved_objects.length >= 6) {
// delete fifth status and prepare to insert a newer one.
const toDelete = ruleStatusSavedObjects.saved_objects.slice(5);
await toDelete.forEach(async item =>
services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id)
);
}
}
},
};

View file

@ -145,3 +145,15 @@ export interface SignalHit {
event: object;
signal: Partial<Signal>;
}
export interface AlertAttributes {
enabled: boolean;
name: string;
tags: string[];
createdBy: string;
createdAt: string;
updatedBy: string;
schedule: {
interval: string;
};
}

View file

@ -179,7 +179,10 @@ describe('utils', () => {
describe('getGapBetweenRuns', () => {
test('it returns a gap of 0 when "from" and interval match each other and the previous started was from the previous interval time', () => {
const gap = getGapBetweenRuns({
previousStartedAt: nowDate.clone().subtract(5, 'minutes'),
previousStartedAt: nowDate
.clone()
.subtract(5, 'minutes')
.toDate(),
interval: '5m',
from: 'now-5m',
to: 'now',
@ -191,7 +194,10 @@ describe('utils', () => {
test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => {
const gap = getGapBetweenRuns({
previousStartedAt: nowDate.clone().subtract(5, 'minutes'),
previousStartedAt: nowDate
.clone()
.subtract(5, 'minutes')
.toDate(),
interval: '5m',
from: 'now-6m',
to: 'now',
@ -203,7 +209,10 @@ describe('utils', () => {
test('it returns a negative gap of 5 minutes when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => {
const gap = getGapBetweenRuns({
previousStartedAt: nowDate.clone().subtract(5, 'minutes'),
previousStartedAt: nowDate
.clone()
.subtract(5, 'minutes')
.toDate(),
interval: '5m',
from: 'now-10m',
to: 'now',
@ -215,7 +224,10 @@ describe('utils', () => {
test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 10 minutes ago and so was the interval', () => {
const gap = getGapBetweenRuns({
previousStartedAt: nowDate.clone().subtract(10, 'minutes'),
previousStartedAt: nowDate
.clone()
.subtract(10, 'minutes')
.toDate(),
interval: '10m',
from: 'now-11m',
to: 'now',
@ -230,7 +242,8 @@ describe('utils', () => {
previousStartedAt: nowDate
.clone()
.subtract(5, 'minutes')
.subtract(30, 'seconds'),
.subtract(30, 'seconds')
.toDate(),
interval: '5m',
from: 'now-6m',
to: 'now',
@ -242,7 +255,10 @@ describe('utils', () => {
test('it returns an exact 0 gap when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is one minute late', () => {
const gap = getGapBetweenRuns({
previousStartedAt: nowDate.clone().subtract(6, 'minutes'),
previousStartedAt: nowDate
.clone()
.subtract(6, 'minutes')
.toDate(),
interval: '5m',
from: 'now-6m',
to: 'now',
@ -257,7 +273,8 @@ describe('utils', () => {
previousStartedAt: nowDate
.clone()
.subtract(6, 'minutes')
.subtract(30, 'seconds'),
.subtract(30, 'seconds')
.toDate(),
interval: '5m',
from: 'now-6m',
to: 'now',
@ -269,7 +286,10 @@ describe('utils', () => {
test('it returns a gap of 1 minute when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is two minutes late', () => {
const gap = getGapBetweenRuns({
previousStartedAt: nowDate.clone().subtract(7, 'minutes'),
previousStartedAt: nowDate
.clone()
.subtract(7, 'minutes')
.toDate(),
interval: '5m',
from: 'now-6m',
to: 'now',
@ -292,7 +312,7 @@ describe('utils', () => {
test('it returns null if the interval is an invalid string such as "invalid"', () => {
const gap = getGapBetweenRuns({
previousStartedAt: nowDate.clone(),
previousStartedAt: nowDate.clone().toDate(),
interval: 'invalid', // if not set to "x" where x is an interval such as 6m
from: 'now-5m',
to: 'now',
@ -303,7 +323,10 @@ describe('utils', () => {
test('it returns the expected result when "from" is an invalid string such as "invalid"', () => {
const gap = getGapBetweenRuns({
previousStartedAt: nowDate.clone().subtract(7, 'minutes'),
previousStartedAt: nowDate
.clone()
.subtract(7, 'minutes')
.toDate(),
interval: '5m',
from: 'invalid',
to: 'now',
@ -315,7 +338,10 @@ describe('utils', () => {
test('it returns the expected result when "to" is an invalid string such as "invalid"', () => {
const gap = getGapBetweenRuns({
previousStartedAt: nowDate.clone().subtract(7, 'minutes'),
previousStartedAt: nowDate
.clone()
.subtract(7, 'minutes')
.toDate(),
interval: '5m',
from: 'now-6m',
to: 'invalid',

View file

@ -69,7 +69,7 @@ export const getGapBetweenRuns = ({
to,
now = moment(),
}: {
previousStartedAt: moment.Moment | undefined | null;
previousStartedAt: Date | undefined | null;
interval: string;
from: string;
to: string;

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObject } from 'src/core/server';
import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings';
import { AlertServices } from '../../../../../../../plugins/alerting/server';
import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types';
interface GetRuleStatusSavedObject {
services: AlertServices;
currentStatusSavedObject: SavedObject<IRuleSavedAttributesSavedObjectAttributes>;
}
export const writeCurrentStatusSucceeded = async ({
services,
currentStatusSavedObject,
}: GetRuleStatusSavedObject): Promise<void> => {
const sDate = new Date().toISOString();
currentStatusSavedObject.attributes.status = 'succeeded';
currentStatusSavedObject.attributes.statusDate = sDate;
currentStatusSavedObject.attributes.lastSuccessAt = sDate;
currentStatusSavedObject.attributes.lastSuccessMessage = 'succeeded';
await services.savedObjectsClient.update(ruleStatusSavedObjectType, currentStatusSavedObject.id, {
...currentStatusSavedObject.attributes,
});
};

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import moment from 'moment';
import { Logger, SavedObject, SavedObjectsFindResponse } from 'src/core/server';
import { AlertServices } from '../../../../../../../plugins/alerting/server';
import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types';
import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings';
interface WriteGapErrorToSavedObjectParams {
logger: Logger;
alertId: string;
ruleId: string;
currentStatusSavedObject: SavedObject<IRuleSavedAttributesSavedObjectAttributes>;
ruleStatusSavedObjects: SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes>;
services: AlertServices;
gap: moment.Duration | null | undefined;
name: string;
}
export const writeGapErrorToSavedObject = async ({
alertId,
currentStatusSavedObject,
logger,
services,
ruleStatusSavedObjects,
ruleId,
gap,
name,
}: WriteGapErrorToSavedObjectParams): Promise<void> => {
if (gap != null && gap.asMilliseconds() > 0) {
logger.warn(
`Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.`
);
// write a failure status whenever we have a time gap
// this is a temporary solution until general activity
// monitoring is developed as a feature
const gapDate = new Date().toISOString();
await services.savedObjectsClient.create(ruleStatusSavedObjectType, {
alertId,
statusDate: gapDate,
status: 'failed',
lastFailureAt: gapDate,
lastSuccessAt: currentStatusSavedObject.attributes.lastSuccessAt,
lastFailureMessage: `Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.`,
lastSuccessMessage: currentStatusSavedObject.attributes.lastSuccessMessage,
});
if (ruleStatusSavedObjects.saved_objects.length >= 6) {
// delete fifth status and prepare to insert a newer one.
const toDelete = ruleStatusSavedObjects.saved_objects.slice(5);
await toDelete.forEach(async item =>
services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id)
);
}
}
};

View file

@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Logger, SavedObject, SavedObjectsFindResponse } from 'src/core/server';
import { AlertServices } from '../../../../../../../plugins/alerting/server';
import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types';
import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings';
interface SignalRuleExceptionParams {
logger: Logger;
alertId: string;
ruleId: string;
currentStatusSavedObject: SavedObject<IRuleSavedAttributesSavedObjectAttributes>;
ruleStatusSavedObjects: SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes>;
message: string;
services: AlertServices;
name: string;
}
export const writeSignalRuleExceptionToSavedObject = async ({
alertId,
currentStatusSavedObject,
logger,
message,
services,
ruleStatusSavedObjects,
ruleId,
name,
}: SignalRuleExceptionParams): Promise<void> => {
logger.error(
`Error from signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" message: ${message}`
);
const sDate = new Date().toISOString();
currentStatusSavedObject.attributes.status = 'failed';
currentStatusSavedObject.attributes.statusDate = sDate;
currentStatusSavedObject.attributes.lastFailureAt = sDate;
currentStatusSavedObject.attributes.lastFailureMessage = message;
// current status is failing
await services.savedObjectsClient.update(ruleStatusSavedObjectType, currentStatusSavedObject.id, {
...currentStatusSavedObject.attributes,
});
// create new status for historical purposes
await services.savedObjectsClient.create(ruleStatusSavedObjectType, {
...currentStatusSavedObject.attributes,
});
if (ruleStatusSavedObjects.saved_objects.length >= 6) {
// delete fifth status and prepare to insert a newer one.
const toDelete = ruleStatusSavedObjects.saved_objects.slice(5);
await toDelete.forEach(async item =>
services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id)
);
}
};