mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
83a5b78fa9
commit
ddedf23149
11 changed files with 417 additions and 239 deletions
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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'],
|
||||
});
|
||||
};
|
|
@ -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',
|
||||
}),
|
||||
},
|
||||
];
|
|
@ -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 }),
|
||||
});
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue