diff --git a/packages/kbn-securitysolution-io-ts-types/src/enumeration/index.ts b/packages/kbn-securitysolution-io-ts-types/src/enumeration/index.ts index 917d6d3bc6c01..303380193704f 100644 --- a/packages/kbn-securitysolution-io-ts-types/src/enumeration/index.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/enumeration/index.ts @@ -8,6 +8,14 @@ import * as t from 'io-ts'; +/** + * Converts string value to a Typescript enum + * - "foo" -> MyEnum.foo + * + * @param name Enum name + * @param originalEnum Typescript enum + * @returns Codec + */ export function enumeration( name: string, originalEnum: Record diff --git a/x-pack/plugins/security_solution/common/utils/invariant.ts b/x-pack/plugins/security_solution/common/utils/invariant.ts index c18c1496afd7d..1b3609ec34642 100644 --- a/x-pack/plugins/security_solution/common/utils/invariant.ts +++ b/x-pack/plugins/security_solution/common/utils/invariant.ts @@ -6,7 +6,7 @@ */ export class InvariantError extends Error { - name = 'Invariant violation'; + name = 'InvariantError'; } /** diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index b6d6a8200aba1..3c069ec048688 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -14,14 +14,14 @@ import { import { rulesClientMock } from '../../../../../../alerting/server/mocks'; import { licensingMock } from '../../../../../../licensing/server/mocks'; import { siemMock } from '../../../../mocks'; -import { RuleExecutionLogClient } from '../../rule_execution_log/__mocks__/rule_execution_log_client'; +import { ruleExecutionLogClientMock } from '../../rule_execution_log/__mocks__/rule_execution_log_client'; const createMockClients = () => ({ rulesClient: rulesClientMock.create(), licensing: { license: licensingMock.createLicenseMock() }, clusterClient: elasticsearchServiceMock.createScopedClusterClient(), savedObjectsClient: savedObjectsClientMock.create(), - ruleExecutionLogClient: new RuleExecutionLogClient(), + ruleExecutionLogClient: ruleExecutionLogClientMock.create(), appClient: siemMock.createClient(), }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts index 026c3fe973366..301cf8518b838 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts @@ -17,7 +17,6 @@ import { } from '../__mocks__/request_responses'; import { findRulesRoute } from './find_rules_route'; -jest.mock('../../signals/rule_status_service'); describe('find_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts index 009c5ac56a009..d9b6f4dd0f10c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts @@ -17,8 +17,6 @@ import { RuleStatusResponse } from '../../rules/types'; import { AlertExecutionStatusErrorReasons } from '../../../../../../alerting/common'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; -jest.mock('../../signals/rule_status_service'); - describe('find_statuses', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts index 475b83a6a29cc..bc9723e47a9d0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts @@ -7,16 +7,17 @@ import { IRuleExecutionLogClient } from '../types'; +export const ruleExecutionLogClientMock = { + create: (): jest.Mocked => ({ + find: jest.fn(), + findBulk: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + logStatusChange: jest.fn(), + logExecutionMetric: jest.fn(), + }), +}; + export const RuleExecutionLogClient = jest .fn, []>() - .mockImplementation(() => { - return { - find: jest.fn(), - findBulk: jest.fn(), - create: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - logStatusChange: jest.fn(), - logExecutionMetric: jest.fn(), - }; - }); + .mockImplementation(ruleExecutionLogClientMock.create); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/saved_objects_adapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/saved_objects_adapter.ts deleted file mode 100644 index 444e11dc5b9f0..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/saved_objects_adapter.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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 { SavedObjectsClientContract } from '../../../../../../../../src/core/server'; -import { - RuleStatusSavedObjectsClient, - ruleStatusSavedObjectsClientFactory, -} from '../../signals/rule_status_saved_objects_client'; -import { - CreateExecutionLogArgs, - ExecutionMetric, - ExecutionMetricArgs, - FindBulkExecutionLogArgs, - FindExecutionLogArgs, - IRuleExecutionLogClient, - LogStatusChangeArgs, - UpdateExecutionLogArgs, -} from '../types'; - -export class SavedObjectsAdapter implements IRuleExecutionLogClient { - private ruleStatusClient: RuleStatusSavedObjectsClient; - - constructor(savedObjectsClient: SavedObjectsClientContract) { - this.ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); - } - - public find({ ruleId, logsCount = 1 }: FindExecutionLogArgs) { - return this.ruleStatusClient.find({ - perPage: logsCount, - sortField: 'statusDate', - sortOrder: 'desc', - search: ruleId, - searchFields: ['alertId'], - }); - } - - public findBulk({ ruleIds, logsCount = 1 }: FindBulkExecutionLogArgs) { - return this.ruleStatusClient.findBulk(ruleIds, logsCount); - } - - public async create({ attributes }: CreateExecutionLogArgs) { - return this.ruleStatusClient.create(attributes); - } - - public async update({ id, attributes }: UpdateExecutionLogArgs) { - await this.ruleStatusClient.update(id, attributes); - } - - public async delete(id: string) { - await this.ruleStatusClient.delete(id); - } - - public async logExecutionMetric(args: ExecutionMetricArgs) { - // TODO These methods are intended to supersede ones provided by RuleStatusService - } - - public async logStatusChange(args: LogStatusChangeArgs) { - // TODO These methods are intended to supersede ones provided by RuleStatusService - } -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts index 26b36c367bda6..135cefe2243b2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts @@ -6,10 +6,9 @@ */ import { SavedObjectsClientContract } from '../../../../../../../src/core/server'; -import { RuleRegistryAdapter } from './adapters/rule_registry_adapter'; -import { SavedObjectsAdapter } from './adapters/saved_objects_adapter'; +import { RuleRegistryAdapter } from './rule_registry_adapter/rule_registry_adapter'; +import { SavedObjectsAdapter } from './saved_objects_adapter/saved_objects_adapter'; import { - CreateExecutionLogArgs, ExecutionMetric, ExecutionMetricArgs, FindBulkExecutionLogArgs, @@ -46,10 +45,6 @@ export class RuleExecutionLogClient implements IRuleExecutionLogClient { return this.client.findBulk(args); } - public async create(args: CreateExecutionLogArgs) { - return this.client.create(args); - } - public async update(args: UpdateExecutionLogArgs) { return this.client.update(args); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/rule_registry_adapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_adapter.ts similarity index 91% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/rule_registry_adapter.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_adapter.ts index 90574528a9338..ab8664ae995bf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/rule_registry_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_adapter.ts @@ -7,7 +7,7 @@ import { merge } from 'lodash'; import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { RuleRegistryLogClient } from '../rule_registry_log_client/rule_registry_log_client'; +import { RuleRegistryLogClient } from './rule_registry_log_client/rule_registry_log_client'; import { CreateExecutionLogArgs, ExecutionMetric, @@ -59,7 +59,7 @@ export class RuleRegistryAdapter implements IRuleExecutionLogClient { return merge(statusesById, lastErrorsById); } - public async create({ attributes, spaceId }: CreateExecutionLogArgs) { + private async create({ attributes, spaceId }: CreateExecutionLogArgs) { if (attributes.status) { await this.ruleRegistryClient.logStatusChange({ ruleId: attributes.alertId, @@ -85,14 +85,6 @@ export class RuleRegistryAdapter implements IRuleExecutionLogClient { spaceId, }); } - - return { - id: '', - type: '', - score: 0, - attributes, - references: [], - }; } public async update({ attributes, spaceId }: UpdateExecutionLogArgs) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/constants.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/constants.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/constants.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/constants.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/parse_rule_execution_log.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/parse_rule_execution_log.ts similarity index 86% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/parse_rule_execution_log.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/parse_rule_execution_log.ts index ed556e312c5df..0c533ed026901 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/parse_rule_execution_log.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/parse_rule_execution_log.ts @@ -7,11 +7,11 @@ import { isLeft } from 'fp-ts/lib/Either'; import { PathReporter } from 'io-ts/lib/PathReporter'; -import { technicalRuleFieldMap } from '../../../../../../rule_registry/common/assets/field_maps/technical_rule_field_map'; +import { technicalRuleFieldMap } from '../../../../../../../rule_registry/common/assets/field_maps/technical_rule_field_map'; import { mergeFieldMaps, runtimeTypeFromFieldMap, -} from '../../../../../../rule_registry/common/field_map'; +} from '../../../../../../../rule_registry/common/field_map'; import { ruleExecutionFieldMap } from './rule_execution_field_map'; const ruleExecutionLogRuntimeType = runtimeTypeFromFieldMap( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_field_map.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_execution_field_map.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_field_map.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_execution_field_map.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_registry_log_client.ts similarity index 93% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_registry_log_client.ts index 5445184c450fe..fd78cac641a46 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_registry_log_client.ts @@ -17,19 +17,19 @@ import { } from '@kbn/rule-data-utils'; import moment from 'moment'; -import { mappingFromFieldMap } from '../../../../../../rule_registry/common/mapping_from_field_map'; -import { Dataset, IRuleDataClient } from '../../../../../../rule_registry/server'; -import { SERVER_APP_ID } from '../../../../../common/constants'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { invariant } from '../../../../../common/utils/invariant'; -import { IRuleStatusSOAttributes } from '../../rules/types'; -import { makeFloatString } from '../../signals/utils'; +import { mappingFromFieldMap } from '../../../../../../../rule_registry/common/mapping_from_field_map'; +import { Dataset, IRuleDataClient } from '../../../../../../../rule_registry/server'; +import { SERVER_APP_ID } from '../../../../../../common/constants'; +import { RuleExecutionStatus } from '../../../../../../common/detection_engine/schemas/common/schemas'; +import { invariant } from '../../../../../../common/utils/invariant'; +import { IRuleStatusSOAttributes } from '../../../rules/types'; +import { makeFloatString } from '../../../signals/utils'; import { ExecutionMetric, ExecutionMetricArgs, IRuleDataPluginService, LogStatusChangeArgs, -} from '../types'; +} from '../../types'; import { EVENT_SEQUENCE, MESSAGE, RULE_STATUS, RULE_STATUS_SEVERITY } from './constants'; import { parseRuleExecutionLog, RuleExecutionEvent } from './parse_rule_execution_log'; import { ruleExecutionFieldMap } from './rule_execution_field_map'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/utils.ts similarity index 93% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/utils.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/utils.ts index 4efbaa91dbda4..713cf73890e7f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/utils.ts @@ -7,8 +7,8 @@ import { SearchSort } from '@elastic/elasticsearch/api/types'; import { EVENT_ACTION, TIMESTAMP } from '@kbn/rule-data-utils'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { ExecutionMetric } from '../types'; +import { RuleExecutionStatus } from '../../../../../../common/detection_engine/schemas/common/schemas'; +import { ExecutionMetric } from '../../types'; import { RULE_STATUS, EVENT_SEQUENCE, EVENT_DURATION, EVENT_END } from './constants'; const METRIC_FIELDS = { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/rule_status_saved_objects_client.ts similarity index 92% rename from x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/rule_status_saved_objects_client.ts index b745009185524..720659b72194f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/rule_status_saved_objects_client.ts @@ -12,10 +12,10 @@ import { SavedObjectsUpdateResponse, SavedObjectsFindOptions, SavedObjectsFindResult, -} from '../../../../../../../src/core/server'; -import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; -import { IRuleStatusSOAttributes } from '../rules/types'; -import { buildChunkedOrFilter } from './utils'; +} from '../../../../../../../../src/core/server'; +import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { IRuleStatusSOAttributes } from '../../rules/types'; +import { buildChunkedOrFilter } from '../../signals/utils'; export interface RuleStatusSavedObjectsClient { find: ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts new file mode 100644 index 0000000000000..27329ebf8f90c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts @@ -0,0 +1,192 @@ +/* + * 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 { SavedObject } from 'src/core/server'; +import { SavedObjectsClientContract } from '../../../../../../../../src/core/server'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { IRuleStatusSOAttributes } from '../../rules/types'; +import { + RuleStatusSavedObjectsClient, + ruleStatusSavedObjectsClientFactory, +} from './rule_status_saved_objects_client'; +import { + ExecutionMetric, + ExecutionMetricArgs, + FindBulkExecutionLogArgs, + FindExecutionLogArgs, + IRuleExecutionLogClient, + LegacyMetrics, + LogStatusChangeArgs, + UpdateExecutionLogArgs, +} from '../types'; +import { assertUnreachable } from '../../../../../common'; + +// 1st is mutable status, followed by 5 most recent failures +export const MAX_RULE_STATUSES = 6; + +const METRIC_FIELDS = { + [ExecutionMetric.executionGap]: 'gap', + [ExecutionMetric.searchDurationMax]: 'searchAfterTimeDurations', + [ExecutionMetric.indexingDurationMax]: 'bulkCreateTimeDurations', + [ExecutionMetric.indexingLookback]: 'lastLookBackDate', +} as const; + +const getMetricField = (metric: T) => METRIC_FIELDS[metric]; + +export class SavedObjectsAdapter implements IRuleExecutionLogClient { + private ruleStatusClient: RuleStatusSavedObjectsClient; + + constructor(savedObjectsClient: SavedObjectsClientContract) { + this.ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + } + + public find({ ruleId, logsCount = 1 }: FindExecutionLogArgs) { + return this.ruleStatusClient.find({ + perPage: logsCount, + sortField: 'statusDate', + sortOrder: 'desc', + search: ruleId, + searchFields: ['alertId'], + }); + } + + public findBulk({ ruleIds, logsCount = 1 }: FindBulkExecutionLogArgs) { + return this.ruleStatusClient.findBulk(ruleIds, logsCount); + } + + public async update({ id, attributes }: UpdateExecutionLogArgs) { + await this.ruleStatusClient.update(id, attributes); + } + + public async delete(id: string) { + await this.ruleStatusClient.delete(id); + } + + public async logExecutionMetric({ + ruleId, + metric, + value, + }: ExecutionMetricArgs) { + const [currentStatus] = await this.getOrCreateRuleStatuses(ruleId); + + await this.ruleStatusClient.update(currentStatus.id, { + ...currentStatus.attributes, + [getMetricField(metric)]: value, + }); + } + + private createNewRuleStatus = async ( + ruleId: string + ): Promise> => { + const now = new Date().toISOString(); + return this.ruleStatusClient.create({ + alertId: ruleId, + statusDate: now, + status: RuleExecutionStatus['going to run'], + lastFailureAt: null, + lastSuccessAt: null, + lastFailureMessage: null, + lastSuccessMessage: null, + gap: null, + bulkCreateTimeDurations: [], + searchAfterTimeDurations: [], + lastLookBackDate: null, + }); + }; + + private getOrCreateRuleStatuses = async ( + ruleId: string + ): Promise>> => { + const ruleStatuses = await this.find({ + spaceId: '', // spaceId is a required argument but it's not used by savedObjectsClient, any string would work here + ruleId, + logsCount: MAX_RULE_STATUSES, + }); + if (ruleStatuses.length > 0) { + return ruleStatuses; + } + const newStatus = await this.createNewRuleStatus(ruleId); + + return [newStatus]; + }; + + public async logStatusChange({ newStatus, ruleId, message, metrics }: LogStatusChangeArgs) { + switch (newStatus) { + case RuleExecutionStatus['going to run']: + case RuleExecutionStatus.succeeded: + case RuleExecutionStatus.warning: + case RuleExecutionStatus['partial failure']: { + const [currentStatus] = await this.getOrCreateRuleStatuses(ruleId); + + await this.ruleStatusClient.update(currentStatus.id, { + ...currentStatus.attributes, + ...buildRuleStatusAttributes(newStatus, message, metrics), + }); + + return; + } + + case RuleExecutionStatus.failed: { + const ruleStatuses = await this.getOrCreateRuleStatuses(ruleId); + const [currentStatus] = ruleStatuses; + + const failureAttributes = { + ...currentStatus.attributes, + ...buildRuleStatusAttributes(RuleExecutionStatus.failed, message, metrics), + }; + + // We always update the newest status, so to 'persist' a failure we push a copy to the head of the list + await this.ruleStatusClient.update(currentStatus.id, failureAttributes); + const lastStatus = await this.ruleStatusClient.create(failureAttributes); + + // drop oldest failures + const oldStatuses = [lastStatus, ...ruleStatuses].slice(MAX_RULE_STATUSES); + await Promise.all(oldStatuses.map((status) => this.delete(status.id))); + + return; + } + default: + assertUnreachable(newStatus, 'Unknown rule execution status supplied to logStatusChange'); + } + } +} + +const buildRuleStatusAttributes: ( + status: RuleExecutionStatus, + message?: string, + metrics?: LegacyMetrics +) => Partial = (status, message, metrics = {}) => { + const now = new Date().toISOString(); + const baseAttributes: Partial = { + ...metrics, + status: + status === RuleExecutionStatus.warning ? RuleExecutionStatus['partial failure'] : status, + statusDate: now, + }; + + switch (status) { + case RuleExecutionStatus.succeeded: + case RuleExecutionStatus.warning: + case RuleExecutionStatus['partial failure']: { + return { + ...baseAttributes, + lastSuccessAt: now, + lastSuccessMessage: message, + }; + } + case RuleExecutionStatus.failed: { + return { + ...baseAttributes, + lastFailureAt: now, + lastFailureMessage: message, + }; + } + case RuleExecutionStatus['going to run']: { + return baseAttributes; + } + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts index 42b9a3bbd66cc..9c66032f681de 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts @@ -6,7 +6,7 @@ */ import { PublicMethodsOf } from '@kbn/utility-types'; -import { SavedObject, SavedObjectsFindResult } from '../../../../../../../src/core/server'; +import { SavedObjectsFindResult } from '../../../../../../../src/core/server'; import { RuleDataPluginService } from '../../../../../rule_registry/server'; import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; import { IRuleStatusSOAttributes } from '../rules/types'; @@ -39,12 +39,24 @@ export interface FindBulkExecutionLogArgs { logsCount?: number; } +/** + * @deprecated LegacyMetrics are only kept here for backward compatibility + * and should be replaced by ExecutionMetric in the future + */ +export interface LegacyMetrics { + searchAfterTimeDurations?: string[]; + bulkCreateTimeDurations?: string[]; + lastLookBackDate?: string; + gap?: string; +} + export interface LogStatusChangeArgs { ruleId: string; spaceId: string; newStatus: RuleExecutionStatus; namespace?: string; message?: string; + metrics?: LegacyMetrics; } export interface UpdateExecutionLogArgs { @@ -75,10 +87,8 @@ export interface IRuleExecutionLogClient { args: FindExecutionLogArgs ) => Promise>>; findBulk: (args: FindBulkExecutionLogArgs) => Promise; - create: (args: CreateExecutionLogArgs) => Promise>; update: (args: UpdateExecutionLogArgs) => Promise; delete: (id: string) => Promise; - // TODO These methods are intended to supersede ones provided by RuleStatusService logStatusChange: (args: LogStatusChangeArgs) => Promise; logExecutionMetric: (args: ExecutionMetricArgs) => Promise; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/with_rule_execution_log.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/with_rule_execution_log.ts deleted file mode 100644 index a78001ee4f674..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/with_rule_execution_log.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 { Logger } from '@kbn/logging'; -import { - AlertInstanceContext, - AlertTypeParams, - AlertTypeState, -} from '../../../../../alerting/common'; -import { AlertTypeWithExecutor } from '../../../../../rule_registry/server'; -import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; -import { RuleExecutionLogClient } from './rule_execution_log_client'; -import { IRuleDataPluginService, IRuleExecutionLogClient } from './types'; - -export interface ExecutionLogServices { - ruleExecutionLogClient: IRuleExecutionLogClient; - logger: Logger; -} - -type WithRuleExecutionLog = (args: { - logger: Logger; - ruleDataService: IRuleDataPluginService; -}) => < - TState extends AlertTypeState, - TParams extends AlertTypeParams, - TAlertInstanceContext extends AlertInstanceContext, - TServices extends ExecutionLogServices ->( - type: AlertTypeWithExecutor -) => AlertTypeWithExecutor; - -export const withRuleExecutionLogFactory: WithRuleExecutionLog = ({ logger, ruleDataService }) => ( - type -) => { - return { - ...type, - executor: async (options) => { - const ruleExecutionLogClient = new RuleExecutionLogClient({ - ruleDataService, - savedObjectsClient: options.services.savedObjectsClient, - }); - try { - await ruleExecutionLogClient.logStatusChange({ - spaceId: options.spaceId, - ruleId: options.alertId, - newStatus: RuleExecutionStatus['going to run'], - }); - - const state = await type.executor({ - ...options, - services: { - ...options.services, - ruleExecutionLogClient, - logger, - }, - }); - - await ruleExecutionLogClient.logStatusChange({ - spaceId: options.spaceId, - ruleId: options.alertId, - newStatus: RuleExecutionStatus.succeeded, - }); - - return state; - } catch (error) { - logger.error(error); - await ruleExecutionLogClient.logStatusChange({ - spaceId: options.spaceId, - ruleId: options.alertId, - newStatus: RuleExecutionStatus.failed, - message: error.message, - }); - } - }, - }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts index 376a4a29ed89a..8ea695ee9940b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts @@ -12,7 +12,6 @@ import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; import { ListArray } from '@kbn/securitysolution-io-ts-list-types'; import { toError } from '@kbn/securitysolution-list-api'; import { createPersistenceRuleTypeFactory } from '../../../../../rule_registry/server'; -import { ruleStatusServiceFactory } from '../signals/rule_status_service'; import { buildRuleMessageFactory } from './factories/build_rule_message_factory'; import { checkPrivilegesFromEsClient, @@ -33,6 +32,7 @@ import { getNotificationResultsLink } from '../notifications/utils'; import { createResultObject } from './utils'; import { bulkCreateFactory, wrapHitsFactory } from './factories'; import { RuleExecutionLogClient } from '../rule_execution_log/rule_execution_log_client'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; /* eslint-disable complexity */ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({ @@ -63,12 +63,6 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({ const esClient = scopedClusterClient.asCurrentUser; const ruleStatusClient = new RuleExecutionLogClient({ savedObjectsClient, ruleDataService }); - const ruleStatusService = await ruleStatusServiceFactory({ - spaceId, - alertId, - ruleStatusClient, - }); - const ruleSO = await savedObjectsClient.get('alert', alertId); const { @@ -89,7 +83,11 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({ logger.debug(buildRuleMessage(`interval: ${interval}`)); let wroteWarningStatus = false; - await ruleStatusService.goingToRun(); + await ruleStatusClient.logStatusChange({ + spaceId, + ruleId: alertId, + newStatus: RuleExecutionStatus['going to run'], + }); let result = createResultObject(state); @@ -122,22 +120,33 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({ () => tryCatch( () => - hasReadIndexPrivileges(privileges, logger, buildRuleMessage, ruleStatusService), + hasReadIndexPrivileges({ + spaceId, + ruleId: alertId, + privileges, + logger, + buildRuleMessage, + ruleStatusClient, + }), toError ), chain((wroteStatus: unknown) => tryCatch( () => - hasTimestampFields( - wroteStatus as boolean, - hasTimestampOverride ? (timestampOverride as string) : '@timestamp', - name, - timestampFieldCaps, + hasTimestampFields({ + spaceId, + ruleId: alertId, + wroteStatus: wroteStatus as boolean, + timestampField: hasTimestampOverride + ? (timestampOverride as string) + : '@timestamp', + ruleName: name, + timestampFieldCapsResponse: timestampFieldCaps, inputIndices, - ruleStatusService, + ruleStatusClient, logger, - buildRuleMessage - ), + buildRuleMessage, + }), toError ) ) @@ -165,7 +174,13 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({ ); logger.warn(gapMessage); hasError = true; - await ruleStatusService.error(gapMessage, { gap: gapString }); + await ruleStatusClient.logStatusChange({ + spaceId, + ruleId: alertId, + newStatus: RuleExecutionStatus.failed, + message: gapMessage, + metrics: { gap: gapString }, + }); } try { @@ -232,7 +247,12 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({ if (result.warningMessages.length) { const warningMessage = buildRuleMessage(result.warningMessages.join()); - await ruleStatusService.partialFailure(warningMessage); + await ruleStatusClient.logStatusChange({ + spaceId, + ruleId: alertId, + newStatus: RuleExecutionStatus['partial failure'], + message: warningMessage, + }); } if (result.success) { @@ -277,10 +297,16 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({ ); if (!hasError && !wroteWarningStatus && !result.warning) { - await ruleStatusService.success('succeeded', { - bulkCreateTimeDurations: result.bulkCreateTimes, - searchAfterTimeDurations: result.searchAfterTimes, - lastLookBackDate: result.lastLookbackDate?.toISOString(), + await ruleStatusClient.logStatusChange({ + spaceId, + ruleId: alertId, + newStatus: RuleExecutionStatus.succeeded, + message: 'succeeded', + metrics: { + bulkCreateTimeDurations: result.bulkCreateTimes, + searchAfterTimeDurations: result.searchAfterTimes, + lastLookBackDate: result.lastLookbackDate?.toISOString(), + }, }); } @@ -300,10 +326,16 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({ result.errors.join() ); logger.error(errorMessage); - await ruleStatusService.error(errorMessage, { - bulkCreateTimeDurations: result.bulkCreateTimes, - searchAfterTimeDurations: result.searchAfterTimes, - lastLookBackDate: result.lastLookbackDate?.toISOString(), + await ruleStatusClient.logStatusChange({ + spaceId, + ruleId: alertId, + newStatus: RuleExecutionStatus.failed, + message: errorMessage, + metrics: { + bulkCreateTimeDurations: result.bulkCreateTimes, + searchAfterTimeDurations: result.searchAfterTimes, + lastLookBackDate: result.lastLookbackDate?.toISOString(), + }, }); } } catch (error) { @@ -314,10 +346,16 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({ ); logger.error(message); - await ruleStatusService.error(message, { - bulkCreateTimeDurations: result.bulkCreateTimes, - searchAfterTimeDurations: result.searchAfterTimes, - lastLookBackDate: result.lastLookbackDate?.toISOString(), + await ruleStatusClient.logStatusChange({ + spaceId, + ruleId: alertId, + newStatus: RuleExecutionStatus.failed, + message, + metrics: { + bulkCreateTimeDurations: result.bulkCreateTimes, + searchAfterTimeDurations: result.searchAfterTimes, + lastLookBackDate: result.lastLookbackDate?.toISOString(), + }, }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts index 4a9d1b5658317..f13a5a5e0e715 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts @@ -26,14 +26,7 @@ jest.mock('../utils/get_list_client', () => ({ }), })); -jest.mock('../../signals/rule_status_service', () => ({ - ruleStatusServiceFactory: () => ({ - goingToRun: jest.fn(), - success: jest.fn(), - partialFailure: jest.fn(), - error: jest.fn(), - }), -})); +jest.mock('../../rule_execution_log/rule_execution_log_client'); describe('Indicator Match Alerts', () => { const params: Partial = { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts index dfe83e32114d3..903cf6adadd43 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts @@ -22,14 +22,7 @@ jest.mock('../utils/get_list_client', () => ({ }), })); -jest.mock('../../signals/rule_status_service', () => ({ - ruleStatusServiceFactory: () => ({ - goingToRun: jest.fn(), - success: jest.fn(), - partialFailure: jest.fn(), - error: jest.fn(), - }), -})); +jest.mock('../../rule_execution_log/rule_execution_log_client'); describe('Custom query alerts', () => { it('does not send an alert when no events found', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts index 86a60da7808ef..ce9ec2afeb6da 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts @@ -12,20 +12,20 @@ import { deleteNotifications } from '../notifications/delete_notifications'; import { deleteRuleActionsSavedObject } from '../rule_actions/delete_rule_actions_saved_object'; import { SavedObjectsFindResult } from '../../../../../../../src/core/server'; import { IRuleStatusSOAttributes } from './types'; -import { RuleExecutionLogClient } from '../rule_execution_log/__mocks__/rule_execution_log_client'; +import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client'; jest.mock('../notifications/delete_notifications'); jest.mock('../rule_actions/delete_rule_actions_saved_object'); describe('deleteRules', () => { let rulesClient: ReturnType; - let ruleStatusClient: ReturnType; + let ruleStatusClient: ReturnType; let savedObjectsClient: ReturnType; beforeEach(() => { rulesClient = rulesClientMock.create(); savedObjectsClient = savedObjectsClientMock.create(); - ruleStatusClient = new RuleExecutionLogClient(); + ruleStatusClient = ruleExecutionLogClientMock.create(); }); it('should delete the rule along with its notifications, actions, and statuses', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts index 98b39e3a5ff27..3f807c0c6082d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts @@ -9,14 +9,14 @@ import { PatchRulesOptions } from './types'; import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { getAlertMock } from '../routes/__mocks__/request_responses'; import { getMlRuleParams, getQueryRuleParams } from '../schemas/rule_schemas.mock'; -import { RuleExecutionLogClient } from '../rule_execution_log/__mocks__/rule_execution_log_client'; +import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client'; export const getPatchRulesOptionsMock = (): PatchRulesOptions => ({ author: ['Elastic'], buildingBlockType: undefined, rulesClient: rulesClientMock.create(), spaceId: 'default', - ruleStatusClient: new RuleExecutionLogClient(), + ruleStatusClient: ruleExecutionLogClientMock.create(), anomalyThreshold: undefined, description: 'some description', enabled: true, @@ -68,7 +68,7 @@ export const getPatchMlRulesOptionsMock = (): PatchRulesOptions => ({ buildingBlockType: undefined, rulesClient: rulesClientMock.create(), spaceId: 'default', - ruleStatusClient: new RuleExecutionLogClient(), + ruleStatusClient: ruleExecutionLogClientMock.create(), anomalyThreshold: 55, description: 'some description', enabled: true, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts index 556a95d816131..5cc7f068aa06d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts @@ -10,16 +10,16 @@ import { getFindResultWithSingleHit } from '../routes/__mocks__/request_response import { updatePrepackagedRules } from './update_prepacked_rules'; import { patchRules } from './patch_rules'; import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock'; -import { RuleExecutionLogClient } from '../rule_execution_log/__mocks__/rule_execution_log_client'; +import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client'; jest.mock('./patch_rules'); describe('updatePrepackagedRules', () => { let rulesClient: ReturnType; - let ruleStatusClient: ReturnType; + let ruleStatusClient: ReturnType; beforeEach(() => { rulesClient = rulesClientMock.create(); - ruleStatusClient = new RuleExecutionLogClient(); + ruleStatusClient = ruleExecutionLogClientMock.create(); }); it('should omit actions and enabled when calling patchRules', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts index c72b225c2fee2..df9431e00a67c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts @@ -10,13 +10,13 @@ import { getUpdateMachineLearningSchemaMock, getUpdateRulesSchemaMock, } from '../../../../common/detection_engine/schemas/request/rule_schemas.mock'; -import { RuleExecutionLogClient } from '../rule_execution_log/__mocks__/rule_execution_log_client'; +import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client'; import { UpdateRulesOptions } from './types'; export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({ spaceId: 'default', rulesClient: rulesClientMock.create(), - ruleStatusClient: new RuleExecutionLogClient(), + ruleStatusClient: ruleExecutionLogClientMock.create(), defaultOutputIndex: '.siem-signals-default', ruleUpdate: getUpdateRulesSchemaMock(), }); @@ -24,7 +24,7 @@ export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({ export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({ spaceId: 'default', rulesClient: rulesClientMock.create(), - ruleStatusClient: new RuleExecutionLogClient(), + ruleStatusClient: ruleExecutionLogClientMock.create(), defaultOutputIndex: '.siem-signals-default', ruleUpdate: getUpdateMachineLearningSchemaMock(), }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index ed93c41035dca..850eee3993b60 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -15,7 +15,7 @@ import type { WrappedSignalHit, AlertAttributes, } from '../types'; -import { SavedObject, SavedObjectsFindResult } from '../../../../../../../../src/core/server'; +import { SavedObject } from '../../../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks'; import { IRuleStatusSOAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; @@ -744,12 +744,6 @@ export const exampleRuleStatus: () => SavedObject = () version: 'WzgyMiwxXQ==', }); -export const exampleFindRuleStatusResponse: ( - mockStatuses: Array> -) => Array> = ( - mockStatuses = [exampleRuleStatus()] -) => mockStatuses.map((obj) => ({ ...obj, score: 1 })); - export const mockLogger = loggingSystemMock.createLogger(); export const sampleBulkErrorItem = ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/rule_status_saved_objects_client.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/rule_status_saved_objects_client.mock.ts deleted file mode 100644 index 3dd328a949938..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/rule_status_saved_objects_client.mock.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { RuleStatusSavedObjectsClient } from '../rule_status_saved_objects_client'; - -const createMockRuleStatusSavedObjectsClient = (): jest.Mocked => ({ - find: jest.fn(), - findBulk: jest.fn(), - create: jest.fn(), - update: jest.fn(), - delete: jest.fn(), -}); - -export const ruleStatusSavedObjectsClientMock = { - create: createMockRuleStatusSavedObjectsClient, -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts deleted file mode 100644 index 0390c073354a6..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 { SavedObject } from 'src/core/server'; - -import { IRuleStatusSOAttributes } from '../rules/types'; -import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; -import { IRuleExecutionLogClient } from '../rule_execution_log/types'; -import { MAX_RULE_STATUSES } from './rule_status_service'; - -interface RuleStatusParams { - alertId: string; - spaceId: string; - ruleStatusClient: IRuleExecutionLogClient; -} - -export const createNewRuleStatus = async ({ - alertId, - spaceId, - ruleStatusClient, -}: RuleStatusParams): Promise> => { - const now = new Date().toISOString(); - return ruleStatusClient.create({ - spaceId, - attributes: { - alertId, - statusDate: now, - status: RuleExecutionStatus['going to run'], - lastFailureAt: null, - lastSuccessAt: null, - lastFailureMessage: null, - lastSuccessMessage: null, - gap: null, - bulkCreateTimeDurations: [], - searchAfterTimeDurations: [], - lastLookBackDate: null, - }, - }); -}; - -export const getOrCreateRuleStatuses = async ({ - spaceId, - alertId, - ruleStatusClient, -}: RuleStatusParams): Promise>> => { - const ruleStatuses = await ruleStatusClient.find({ - spaceId, - ruleId: alertId, - logsCount: MAX_RULE_STATUSES, - }); - if (ruleStatuses.length > 0) { - return ruleStatuses; - } - const newStatus = await createNewRuleStatus({ alertId, spaceId, ruleStatusClient }); - - return [newStatus]; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts deleted file mode 100644 index 1ecdf09880873..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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 { RuleStatusService } from './rule_status_service'; - -export const getRuleStatusServiceMock = (): jest.Mocked => ({ - goingToRun: jest.fn(), - success: jest.fn(), - partialFailure: jest.fn(), - error: jest.fn(), -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts deleted file mode 100644 index 9a36dd0103a60..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* - * 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 { - buildRuleStatusAttributes, - RuleStatusService, - ruleStatusServiceFactory, - MAX_RULE_STATUSES, -} from './rule_status_service'; -import { exampleRuleStatus, exampleFindRuleStatusResponse } from './__mocks__/es_results'; -import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; -import { RuleExecutionLogClient } from '../rule_execution_log/__mocks__/rule_execution_log_client'; -import { UpdateExecutionLogArgs } from '../rule_execution_log/types'; - -const expectIsoDateString = expect.stringMatching(/2.*Z$/); -const buildStatuses = (n: number) => - Array(n) - .fill(exampleRuleStatus()) - .map((status, index) => ({ - ...status, - id: `status-index-${index}`, - })); - -describe('buildRuleStatusAttributes', () => { - it('generates a new date on each call', async () => { - const { statusDate } = buildRuleStatusAttributes(RuleExecutionStatus['going to run']); - await new Promise((resolve) => setTimeout(resolve, 10)); // ensure time has passed - const { statusDate: statusDate2 } = buildRuleStatusAttributes( - RuleExecutionStatus['going to run'] - ); - - expect(statusDate).toEqual(expectIsoDateString); - expect(statusDate2).toEqual(expectIsoDateString); - expect(statusDate).not.toEqual(statusDate2); - }); - - it('returns a status and statusDate if "going to run"', () => { - const result = buildRuleStatusAttributes(RuleExecutionStatus['going to run']); - expect(result).toEqual({ - status: 'going to run', - statusDate: expectIsoDateString, - }); - }); - - it('returns success fields if "success"', () => { - const result = buildRuleStatusAttributes(RuleExecutionStatus.succeeded, 'success message'); - expect(result).toEqual({ - status: 'succeeded', - statusDate: expectIsoDateString, - lastSuccessAt: expectIsoDateString, - lastSuccessMessage: 'success message', - }); - - expect(result.statusDate).toEqual(result.lastSuccessAt); - }); - - it('returns warning fields if "warning"', () => { - const result = buildRuleStatusAttributes( - RuleExecutionStatus.warning, - 'some indices missing timestamp override field' - ); - expect(result).toEqual({ - status: 'warning', - statusDate: expectIsoDateString, - lastSuccessAt: expectIsoDateString, - lastSuccessMessage: 'some indices missing timestamp override field', - }); - - expect(result.statusDate).toEqual(result.lastSuccessAt); - }); - - it('returns failure fields if "failed"', () => { - const result = buildRuleStatusAttributes(RuleExecutionStatus.failed, 'failure message'); - expect(result).toEqual({ - status: 'failed', - statusDate: expectIsoDateString, - lastFailureAt: expectIsoDateString, - lastFailureMessage: 'failure message', - }); - - expect(result.statusDate).toEqual(result.lastFailureAt); - }); -}); - -describe('ruleStatusService', () => { - let currentStatus: ReturnType; - let ruleStatusClient: ReturnType; - let service: RuleStatusService; - - beforeEach(async () => { - currentStatus = exampleRuleStatus(); - ruleStatusClient = new RuleExecutionLogClient(); - ruleStatusClient.find.mockResolvedValue(exampleFindRuleStatusResponse([currentStatus])); - service = await ruleStatusServiceFactory({ - alertId: 'mock-alert-id', - ruleStatusClient, - spaceId: 'default', - }); - }); - - describe('goingToRun', () => { - it('updates the current status to "going to run"', async () => { - await service.goingToRun(); - - expect(ruleStatusClient.update).toHaveBeenCalledWith<[UpdateExecutionLogArgs]>({ - id: currentStatus.id, - spaceId: 'default', - attributes: expect.objectContaining({ - status: 'going to run', - statusDate: expectIsoDateString, - }), - }); - }); - }); - - describe('success', () => { - it('updates the current status to "succeeded"', async () => { - await service.success('hey, it worked'); - - expect(ruleStatusClient.update).toHaveBeenCalledWith<[UpdateExecutionLogArgs]>({ - id: currentStatus.id, - spaceId: 'default', - attributes: expect.objectContaining({ - status: 'succeeded', - statusDate: expectIsoDateString, - lastSuccessAt: expectIsoDateString, - lastSuccessMessage: 'hey, it worked', - }), - }); - }); - }); - - describe('error', () => { - beforeEach(() => { - // mock the creation of our new status - ruleStatusClient.create.mockResolvedValue(exampleRuleStatus()); - }); - - it('updates the current status to "failed"', async () => { - await service.error('oh no, it broke'); - - expect(ruleStatusClient.update).toHaveBeenCalledWith<[UpdateExecutionLogArgs]>({ - id: currentStatus.id, - spaceId: 'default', - attributes: expect.objectContaining({ - status: 'failed', - statusDate: expectIsoDateString, - lastFailureAt: expectIsoDateString, - lastFailureMessage: 'oh no, it broke', - }), - }); - }); - - it('does not delete statuses if we have less than the max number of statuses', async () => { - await service.error('oh no, it broke'); - - expect(ruleStatusClient.delete).not.toHaveBeenCalled(); - }); - - it('does not delete rule statuses when we just hit the limit', async () => { - // max - 1 in store, meaning our new error will put us at max - ruleStatusClient.find.mockResolvedValue( - exampleFindRuleStatusResponse(buildStatuses(MAX_RULE_STATUSES - 1)) - ); - service = await ruleStatusServiceFactory({ - alertId: 'mock-alert-id', - ruleStatusClient, - spaceId: 'default', - }); - - await service.error('oh no, it broke'); - - expect(ruleStatusClient.delete).not.toHaveBeenCalled(); - }); - - it('deletes stale rule status when we already have max statuses', async () => { - // max in store, meaning our new error will push one off the end - ruleStatusClient.find.mockResolvedValue( - exampleFindRuleStatusResponse(buildStatuses(MAX_RULE_STATUSES)) - ); - service = await ruleStatusServiceFactory({ - alertId: 'mock-alert-id', - ruleStatusClient, - spaceId: 'default', - }); - - await service.error('oh no, it broke'); - - expect(ruleStatusClient.delete).toHaveBeenCalledTimes(1); - // we should delete the 6th (index 5) - expect(ruleStatusClient.delete).toHaveBeenCalledWith('status-index-5'); - }); - - it('deletes any number of rule statuses in excess of the max', async () => { - // max + 1 in store, meaning our new error will put us two over - ruleStatusClient.find.mockResolvedValue( - exampleFindRuleStatusResponse(buildStatuses(MAX_RULE_STATUSES + 1)) - ); - service = await ruleStatusServiceFactory({ - alertId: 'mock-alert-id', - ruleStatusClient, - spaceId: 'default', - }); - - await service.error('oh no, it broke'); - - expect(ruleStatusClient.delete).toHaveBeenCalledTimes(2); - // we should delete the 6th (index 5) - expect(ruleStatusClient.delete).toHaveBeenCalledWith('status-index-5'); - // we should delete the 7th (index 6) - expect(ruleStatusClient.delete).toHaveBeenCalledWith('status-index-6'); - }); - - it('handles multiple error calls', async () => { - // max in store, meaning our new error will push one off the end - ruleStatusClient.find.mockResolvedValue( - exampleFindRuleStatusResponse(buildStatuses(MAX_RULE_STATUSES)) - ); - service = await ruleStatusServiceFactory({ - alertId: 'mock-alert-id', - ruleStatusClient, - spaceId: 'default', - }); - - await service.error('oh no, it broke'); - await service.error('oh no, it broke'); - - expect(ruleStatusClient.delete).toHaveBeenCalledTimes(2); - // we should delete the 6th (index 5) - expect(ruleStatusClient.delete).toHaveBeenCalledWith('status-index-5'); - expect(ruleStatusClient.delete).toHaveBeenCalledWith('status-index-5'); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts deleted file mode 100644 index 45eff57d304e6..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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 { assertUnreachable } from '../../../../common/utility_types'; -import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; -import { IRuleStatusSOAttributes } from '../rules/types'; -import { getOrCreateRuleStatuses } from './get_or_create_rule_statuses'; -import { IRuleExecutionLogClient } from '../rule_execution_log/types'; - -// 1st is mutable status, followed by 5 most recent failures -export const MAX_RULE_STATUSES = 6; - -interface Attributes { - searchAfterTimeDurations?: string[]; - bulkCreateTimeDurations?: string[]; - lastLookBackDate?: string; - gap?: string; -} - -export interface RuleStatusService { - goingToRun: () => Promise; - success: (message: string, attributes?: Attributes) => Promise; - partialFailure: (message: string, attributes?: Attributes) => Promise; - error: (message: string, attributes?: Attributes) => Promise; -} - -export const buildRuleStatusAttributes: ( - status: RuleExecutionStatus, - message?: string, - attributes?: Attributes -) => Partial = (status, message, attributes = {}) => { - const now = new Date().toISOString(); - const baseAttributes: Partial = { - ...attributes, - status, - statusDate: now, - }; - - switch (status) { - case RuleExecutionStatus.succeeded: { - return { - ...baseAttributes, - lastSuccessAt: now, - lastSuccessMessage: message, - }; - } - case RuleExecutionStatus.warning: { - return { - ...baseAttributes, - lastSuccessAt: now, - lastSuccessMessage: message, - }; - } - case RuleExecutionStatus['partial failure']: { - return { - ...baseAttributes, - lastSuccessAt: now, - lastSuccessMessage: message, - }; - } - case RuleExecutionStatus.failed: { - return { - ...baseAttributes, - lastFailureAt: now, - lastFailureMessage: message, - }; - } - case RuleExecutionStatus['going to run']: { - return baseAttributes; - } - } - - assertUnreachable(status); -}; - -export const ruleStatusServiceFactory = async ({ - spaceId, - alertId, - ruleStatusClient, -}: { - spaceId: string; - alertId: string; - ruleStatusClient: IRuleExecutionLogClient; -}): Promise => { - return { - goingToRun: async () => { - const [currentStatus] = await getOrCreateRuleStatuses({ - spaceId, - alertId, - ruleStatusClient, - }); - - await ruleStatusClient.update({ - id: currentStatus.id, - attributes: { - ...currentStatus.attributes, - ...buildRuleStatusAttributes(RuleExecutionStatus['going to run']), - }, - spaceId, - }); - }, - - success: async (message, attributes) => { - const [currentStatus] = await getOrCreateRuleStatuses({ - spaceId, - alertId, - ruleStatusClient, - }); - - await ruleStatusClient.update({ - id: currentStatus.id, - attributes: { - ...currentStatus.attributes, - ...buildRuleStatusAttributes(RuleExecutionStatus.succeeded, message, attributes), - }, - spaceId, - }); - }, - - partialFailure: async (message, attributes) => { - const [currentStatus] = await getOrCreateRuleStatuses({ - spaceId, - alertId, - ruleStatusClient, - }); - - await ruleStatusClient.update({ - id: currentStatus.id, - attributes: { - ...currentStatus.attributes, - ...buildRuleStatusAttributes(RuleExecutionStatus['partial failure'], message, attributes), - }, - spaceId, - }); - }, - - error: async (message, attributes) => { - const ruleStatuses = await getOrCreateRuleStatuses({ - spaceId, - alertId, - ruleStatusClient, - }); - const [currentStatus] = ruleStatuses; - - const failureAttributes = { - ...currentStatus.attributes, - ...buildRuleStatusAttributes(RuleExecutionStatus.failed, message, attributes), - }; - - // We always update the newest status, so to 'persist' a failure we push a copy to the head of the list - await ruleStatusClient.update({ - id: currentStatus.id, - attributes: failureAttributes, - spaceId, - }); - const newStatus = await ruleStatusClient.create({ attributes: failureAttributes, spaceId }); - - // drop oldest failures - const oldStatuses = [newStatus, ...ruleStatuses].slice(MAX_RULE_STATUSES); - await Promise.all(oldStatuses.map((status) => ruleStatusClient.delete(status.id))); - }, - }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 6435204d1b7df..df2ccf61c3f29 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -11,7 +11,6 @@ import { loggingSystemMock } from 'src/core/server/mocks'; import { getAlertMock } from '../routes/__mocks__/request_responses'; import { signalRulesAlertType } from './signal_rule_alert_type'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; -import { ruleStatusServiceFactory } from './rule_status_service'; import { getListsClient, getExceptions, @@ -35,9 +34,9 @@ import { getMlRuleParams, getQueryRuleParams } from '../schemas/rule_schemas.moc import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { allowedExperimentalValues } from '../../../../common/experimental_features'; import { ruleRegistryMocks } from '../../../../../rule_registry/server/mocks'; +import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; -jest.mock('./rule_status_saved_objects_client'); -jest.mock('./rule_status_service'); jest.mock('./utils', () => { const original = jest.requireActual('./utils'); return { @@ -59,6 +58,12 @@ jest.mock('@kbn/securitysolution-io-ts-utils', () => { }; }); +const mockRuleExecutionLogClient = ruleExecutionLogClientMock.create(); + +jest.mock('../rule_execution_log/rule_execution_log_client', () => ({ + RuleExecutionLogClient: jest.fn().mockImplementation(() => mockRuleExecutionLogClient), +})); + const getPayload = ( ruleAlert: RuleAlertType, services: AlertServicesMock @@ -119,21 +124,12 @@ describe('signal_rule_alert_type', () => { let alert: ReturnType; let logger: ReturnType; let alertServices: AlertServicesMock; - let ruleStatusService: Record; let ruleDataService: ReturnType; beforeEach(() => { alertServices = alertsMock.createAlertServices(); logger = loggingSystemMock.createLogger(); - ruleStatusService = { - success: jest.fn(), - find: jest.fn(), - goingToRun: jest.fn(), - error: jest.fn(), - partialFailure: jest.fn(), - }; ruleDataService = ruleRegistryMocks.createRuleDataPluginService(); - (ruleStatusServiceFactory as jest.Mock).mockReturnValue(ruleStatusService); (getListsClient as jest.Mock).mockReturnValue({ listClient: getListClientMock(), exceptionsClient: getExceptionListClientMock(), @@ -201,23 +197,33 @@ describe('signal_rule_alert_type', () => { mergeStrategy: 'missingFields', ruleDataService, }); + + mockRuleExecutionLogClient.logStatusChange.mockClear(); }); describe('executor', () => { - it('should call ruleStatusService.success if signals were created', async () => { + it('should log success status if signals were created', async () => { payload.previousStartedAt = null; await alert.executor(payload); - expect(ruleStatusService.success).toHaveBeenCalled(); + expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + newStatus: RuleExecutionStatus.succeeded, + }) + ); }); it('should warn about the gap between runs if gap is very large', async () => { payload.previousStartedAt = moment().subtract(100, 'm').toDate(); await alert.executor(payload); expect(logger.warn).toHaveBeenCalled(); - expect(ruleStatusService.error).toHaveBeenCalled(); - expect(ruleStatusService.error.mock.calls[0][1]).toEqual({ - gap: 'an hour', - }); + expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + newStatus: RuleExecutionStatus.failed, + metrics: { + gap: 'an hour', + }, + }) + ); }); it('should set a warning for when rules cannot read ALL provided indices', async () => { @@ -243,9 +249,12 @@ describe('signal_rule_alert_type', () => { payload = getPayload(newRuleAlert, alertServices) as jest.Mocked; await alert.executor(payload); - expect(ruleStatusService.partialFailure).toHaveBeenCalled(); - expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain( - 'Missing required read privileges on the following indices: ["some*"]' + expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + newStatus: RuleExecutionStatus['partial failure'], + message: 'Missing required read privileges on the following indices: ["some*"]', + }) ); }); @@ -269,9 +278,13 @@ describe('signal_rule_alert_type', () => { payload = getPayload(newRuleAlert, alertServices) as jest.Mocked; await alert.executor(payload); - expect(ruleStatusService.partialFailure).toHaveBeenCalled(); - expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain( - 'This rule may not have the required read privileges to the following indices: ["myfa*","some*"]' + expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + newStatus: RuleExecutionStatus['partial failure'], + message: + 'This rule may not have the required read privileges to the following indices: ["myfa*","some*"]', + }) ); }); @@ -279,7 +292,19 @@ describe('signal_rule_alert_type', () => { payload.previousStartedAt = moment().subtract(10, 'm').toDate(); await alert.executor(payload); expect(logger.warn).toHaveBeenCalledTimes(0); - expect(ruleStatusService.error).toHaveBeenCalledTimes(0); + expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenCalledTimes(2); + expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + newStatus: RuleExecutionStatus['going to run'], + }) + ); + expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + newStatus: RuleExecutionStatus.succeeded, + }) + ); }); it('should call scheduleActions if signalsCount was greater than 0 and rule has actions defined', async () => { @@ -426,7 +451,11 @@ describe('signal_rule_alert_type', () => { await alert.executor(payload); expect(checkPrivileges).toHaveBeenCalledTimes(0); - expect(ruleStatusService.success).toHaveBeenCalled(); + expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + newStatus: RuleExecutionStatus.succeeded, + }) + ); }); }); }); @@ -450,7 +479,11 @@ describe('signal_rule_alert_type', () => { expect(logger.error.mock.calls[0][0]).toContain( 'Bulk Indexing of signals failed: Error that bubbled up. name: "Detect Root/Admin Users" id: "04128c15-0d1b-4716-a4c5-46997ac7f3bd" rule id: "rule-1" signals index: ".siem-signals"' ); - expect(ruleStatusService.error).toHaveBeenCalled(); + expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + newStatus: RuleExecutionStatus.failed, + }) + ); }); it('when error was thrown', async () => { @@ -458,10 +491,14 @@ describe('signal_rule_alert_type', () => { await alert.executor(payload); expect(logger.error).toHaveBeenCalled(); expect(logger.error.mock.calls[0][0]).toContain('An error occurred during rule execution'); - expect(ruleStatusService.error).toHaveBeenCalled(); + expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + newStatus: RuleExecutionStatus.failed, + }) + ); }); - it('and call ruleStatusService with the default message', async () => { + it('and log failure with the default message', async () => { (queryExecutor as jest.Mock).mockReturnValue( elasticsearchClientMock.createErrorTransportRequestPromise( new ResponseError( @@ -475,7 +512,11 @@ describe('signal_rule_alert_type', () => { await alert.executor(payload); expect(logger.error).toHaveBeenCalled(); expect(logger.error.mock.calls[0][0]).toContain('An error occurred during rule execution'); - expect(ruleStatusService.error).toHaveBeenCalled(); + expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + newStatus: RuleExecutionStatus.failed, + }) + ); }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index b242691577b89..3da9d8538151a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -45,7 +45,6 @@ import { scheduleNotificationActions, NotificationRuleTypeParams, } from '../notifications/schedule_notification_actions'; -import { ruleStatusServiceFactory } from './rule_status_service'; import { buildRuleMessageFactory } from './rule_messages'; import { getNotificationResultsLink } from '../notifications/utils'; import { TelemetryEventsSender } from '../../telemetry/sender'; @@ -72,6 +71,7 @@ import { ExperimentalFeatures } from '../../../../common/experimental_features'; import { injectReferences, extractReferences } from './saved_object_references'; import { RuleExecutionLogClient } from '../rule_execution_log/rule_execution_log_client'; import { IRuleDataPluginService } from '../rule_execution_log/types'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; export const signalRulesAlertType = ({ logger, @@ -137,11 +137,6 @@ export const signalRulesAlertType = ({ ruleDataService, savedObjectsClient: services.savedObjectsClient, }); - const ruleStatusService = await ruleStatusServiceFactory({ - spaceId, - alertId, - ruleStatusClient, - }); const savedObject = await services.savedObjectsClient.get('alert', alertId); const { @@ -160,7 +155,11 @@ export const signalRulesAlertType = ({ logger.debug(buildRuleMessage('[+] Starting Signal Rule execution')); logger.debug(buildRuleMessage(`interval: ${interval}`)); let wroteWarningStatus = false; - await ruleStatusService.goingToRun(); + await ruleStatusClient.logStatusChange({ + ruleId: alertId, + newStatus: RuleExecutionStatus['going to run'], + spaceId, + }); // check if rule has permissions to access given index pattern // move this collection of lines into a function in utils @@ -190,22 +189,33 @@ export const signalRulesAlertType = ({ () => tryCatch( () => - hasReadIndexPrivileges(privileges, logger, buildRuleMessage, ruleStatusService), + hasReadIndexPrivileges({ + spaceId, + ruleId: alertId, + privileges, + logger, + buildRuleMessage, + ruleStatusClient, + }), toError ), chain((wroteStatus) => tryCatch( () => - hasTimestampFields( - wroteStatus, - hasTimestampOverride ? (timestampOverride as string) : '@timestamp', - name, - timestampFieldCaps, + hasTimestampFields({ + spaceId, + ruleId: alertId, + wroteStatus: wroteStatus as boolean, + timestampField: hasTimestampOverride + ? (timestampOverride as string) + : '@timestamp', + ruleName: name, + timestampFieldCapsResponse: timestampFieldCaps, inputIndices, - ruleStatusService, + ruleStatusClient, logger, - buildRuleMessage - ), + buildRuleMessage, + }), toError ) ), @@ -232,7 +242,13 @@ export const signalRulesAlertType = ({ ); logger.warn(gapMessage); hasError = true; - await ruleStatusService.error(gapMessage, { gap: gapString }); + await ruleStatusClient.logStatusChange({ + spaceId, + ruleId: alertId, + newStatus: RuleExecutionStatus.failed, + message: gapMessage, + metrics: { gap: gapString }, + }); } try { const { listClient, exceptionsClient } = getListsClient({ @@ -359,7 +375,12 @@ export const signalRulesAlertType = ({ } if (result.warningMessages.length) { const warningMessage = buildRuleMessage(result.warningMessages.join()); - await ruleStatusService.partialFailure(warningMessage); + await ruleStatusClient.logStatusChange({ + spaceId, + ruleId: alertId, + newStatus: RuleExecutionStatus['partial failure'], + message: warningMessage, + }); } if (result.success) { @@ -403,10 +424,16 @@ export const signalRulesAlertType = ({ ) ); if (!hasError && !wroteWarningStatus && !result.warning) { - await ruleStatusService.success('succeeded', { - bulkCreateTimeDurations: result.bulkCreateTimes, - searchAfterTimeDurations: result.searchAfterTimes, - lastLookBackDate: result.lastLookBackDate?.toISOString(), + await ruleStatusClient.logStatusChange({ + spaceId, + ruleId: alertId, + newStatus: RuleExecutionStatus.succeeded, + message: 'succeeded', + metrics: { + bulkCreateTimeDurations: result.bulkCreateTimes, + searchAfterTimeDurations: result.searchAfterTimes, + lastLookBackDate: result.lastLookBackDate?.toISOString(), + }, }); } @@ -426,10 +453,16 @@ export const signalRulesAlertType = ({ result.errors.join() ); logger.error(errorMessage); - await ruleStatusService.error(errorMessage, { - bulkCreateTimeDurations: result.bulkCreateTimes, - searchAfterTimeDurations: result.searchAfterTimes, - lastLookBackDate: result.lastLookBackDate?.toISOString(), + await ruleStatusClient.logStatusChange({ + spaceId, + ruleId: alertId, + newStatus: RuleExecutionStatus.failed, + message: errorMessage, + metrics: { + bulkCreateTimeDurations: result.bulkCreateTimes, + searchAfterTimeDurations: result.searchAfterTimes, + lastLookBackDate: result.lastLookBackDate?.toISOString(), + }, }); } } catch (error) { @@ -440,10 +473,16 @@ export const signalRulesAlertType = ({ ); logger.error(message); - await ruleStatusService.error(message, { - bulkCreateTimeDurations: result.bulkCreateTimes, - searchAfterTimeDurations: result.searchAfterTimes, - lastLookBackDate: result.lastLookBackDate?.toISOString(), + await ruleStatusClient.logStatusChange({ + spaceId, + ruleId: alertId, + newStatus: RuleExecutionStatus.failed, + message, + metrics: { + bulkCreateTimeDurations: result.bulkCreateTimes, + searchAfterTimeDurations: result.searchAfterTimes, + lastLookBackDate: result.lastLookBackDate?.toISOString(), + }, }); } }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 72a6ff478ade3..40c64bf19f0a1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -58,6 +58,7 @@ import { sampleDocNoSortId, } from './__mocks__/es_results'; import { ShardError } from '../../types'; +import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client'; const buildRuleMessage = buildRuleMessageFactory({ id: 'fake id', @@ -66,13 +67,7 @@ const buildRuleMessage = buildRuleMessageFactory({ name: 'fake name', }); -const ruleStatusServiceMock = { - success: jest.fn(), - find: jest.fn(), - goingToRun: jest.fn(), - error: jest.fn(), - partialFailure: jest.fn(), -}; +const ruleStatusClient = ruleExecutionLogClientMock.create(); describe('utils', () => { const anchor = '2020-01-01T06:06:06.666Z'; @@ -785,17 +780,19 @@ describe('utils', () => { }, }; mockLogger.error.mockClear(); - const res = await hasTimestampFields( - false, + const res = await hasTimestampFields({ + wroteStatus: false, timestampField, - 'myfakerulename', + ruleName: 'myfakerulename', // eslint-disable-next-line @typescript-eslint/no-explicit-any - timestampFieldCapsResponse as ApiResponse>, - ['myfa*'], - ruleStatusServiceMock, - mockLogger, - buildRuleMessage - ); + timestampFieldCapsResponse: timestampFieldCapsResponse as ApiResponse>, + inputIndices: ['myfa*'], + ruleStatusClient, + ruleId: 'ruleId', + spaceId: 'default', + logger: mockLogger, + buildRuleMessage, + }); expect(mockLogger.error).toHaveBeenCalledWith( 'The following indices are missing the timestamp override field "event.ingested": ["myfakeindex-1","myfakeindex-2"] name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); @@ -826,17 +823,19 @@ describe('utils', () => { }, }; mockLogger.error.mockClear(); - const res = await hasTimestampFields( - false, + const res = await hasTimestampFields({ + wroteStatus: false, timestampField, - 'myfakerulename', + ruleName: 'myfakerulename', // eslint-disable-next-line @typescript-eslint/no-explicit-any - timestampFieldCapsResponse as ApiResponse>, - ['myfa*'], - ruleStatusServiceMock, - mockLogger, - buildRuleMessage - ); + timestampFieldCapsResponse: timestampFieldCapsResponse as ApiResponse>, + inputIndices: ['myfa*'], + ruleStatusClient, + ruleId: 'ruleId', + spaceId: 'default', + logger: mockLogger, + buildRuleMessage, + }); expect(mockLogger.error).toHaveBeenCalledWith( 'The following indices are missing the timestamp field "@timestamp": ["myfakeindex-1","myfakeindex-2"] name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); @@ -853,17 +852,19 @@ describe('utils', () => { }, }; mockLogger.error.mockClear(); - const res = await hasTimestampFields( - false, + const res = await hasTimestampFields({ + wroteStatus: false, timestampField, - 'Endpoint Security', + ruleName: 'Endpoint Security', // eslint-disable-next-line @typescript-eslint/no-explicit-any - timestampFieldCapsResponse as ApiResponse>, - ['logs-endpoint.alerts-*'], - ruleStatusServiceMock, - mockLogger, - buildRuleMessage - ); + timestampFieldCapsResponse: timestampFieldCapsResponse as ApiResponse>, + inputIndices: ['logs-endpoint.alerts-*'], + ruleStatusClient, + ruleId: 'ruleId', + spaceId: 'default', + logger: mockLogger, + buildRuleMessage, + }); expect(mockLogger.error).toHaveBeenCalledWith( 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent. name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); @@ -880,17 +881,19 @@ describe('utils', () => { }, }; mockLogger.error.mockClear(); - const res = await hasTimestampFields( - false, + const res = await hasTimestampFields({ + wroteStatus: false, timestampField, - 'NOT Endpoint Security', + ruleName: 'NOT Endpoint Security', // eslint-disable-next-line @typescript-eslint/no-explicit-any - timestampFieldCapsResponse as ApiResponse>, - ['logs-endpoint.alerts-*'], - ruleStatusServiceMock, - mockLogger, - buildRuleMessage - ); + timestampFieldCapsResponse: timestampFieldCapsResponse as ApiResponse>, + inputIndices: ['logs-endpoint.alerts-*'], + ruleStatusClient, + ruleId: 'ruleId', + spaceId: 'default', + logger: mockLogger, + buildRuleMessage, + }); expect(mockLogger.error).toHaveBeenCalledWith( 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 72ac4f6d0f550..554fe87bbf413 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -22,6 +22,7 @@ import { ElasticsearchClient } from '@kbn/securitysolution-es-utils'; import { TimestampOverrideOrUndefined, Privilege, + RuleExecutionStatus, } from '../../../../common/detection_engine/schemas/common/schemas'; import { Logger, SavedObjectsClientContract } from '../../../../../../../src/core/server'; import { @@ -46,7 +47,6 @@ import { } from './types'; import { BuildRuleMessage } from './rule_messages'; import { ShardError } from '../../types'; -import { RuleStatusService } from './rule_status_service'; import { EqlRuleParams, MachineLearningRuleParams, @@ -58,6 +58,7 @@ import { } from '../schemas/rule_schemas'; import { WrappedRACAlert } from '../rule_types/types'; import { SearchTypes } from '../../../../common/detection_engine/types'; +import { IRuleExecutionLogClient } from '../rule_execution_log/types'; interface SortExceptionsReturn { exceptionsWithValueLists: ExceptionListItemSchema[]; @@ -81,12 +82,16 @@ export const shorthandMap = { }, }; -export const hasReadIndexPrivileges = async ( - privileges: Privilege, - logger: Logger, - buildRuleMessage: BuildRuleMessage, - ruleStatusService: RuleStatusService -): Promise => { +export const hasReadIndexPrivileges = async (args: { + privileges: Privilege; + logger: Logger; + buildRuleMessage: BuildRuleMessage; + ruleStatusClient: IRuleExecutionLogClient; + ruleId: string; + spaceId: string; +}): Promise => { + const { privileges, logger, buildRuleMessage, ruleStatusClient, ruleId, spaceId } = args; + const indexNames = Object.keys(privileges.index); const [indexesWithReadPrivileges, indexesWithNoReadPrivileges] = partition( indexNames, @@ -100,7 +105,12 @@ export const hasReadIndexPrivileges = async ( indexesWithNoReadPrivileges )}`; logger.error(buildRuleMessage(errorString)); - await ruleStatusService.partialFailure(errorString); + await ruleStatusClient.logStatusChange({ + message: errorString, + ruleId, + spaceId, + newStatus: RuleExecutionStatus['partial failure'], + }); return true; } else if ( indexesWithReadPrivileges.length === 0 && @@ -112,25 +122,45 @@ export const hasReadIndexPrivileges = async ( indexesWithNoReadPrivileges )}`; logger.error(buildRuleMessage(errorString)); - await ruleStatusService.partialFailure(errorString); + await ruleStatusClient.logStatusChange({ + message: errorString, + ruleId, + spaceId, + newStatus: RuleExecutionStatus['partial failure'], + }); return true; } return false; }; -export const hasTimestampFields = async ( - wroteStatus: boolean, - timestampField: string, - ruleName: string, +export const hasTimestampFields = async (args: { + wroteStatus: boolean; + timestampField: string; + ruleName: string; // any is derived from here // node_modules/@elastic/elasticsearch/api/kibana.d.ts // eslint-disable-next-line @typescript-eslint/no-explicit-any - timestampFieldCapsResponse: ApiResponse, Context>, - inputIndices: string[], - ruleStatusService: RuleStatusService, - logger: Logger, - buildRuleMessage: BuildRuleMessage -): Promise => { + timestampFieldCapsResponse: ApiResponse, Context>; + inputIndices: string[]; + ruleStatusClient: IRuleExecutionLogClient; + ruleId: string; + spaceId: string; + logger: Logger; + buildRuleMessage: BuildRuleMessage; +}): Promise => { + const { + wroteStatus, + timestampField, + ruleName, + timestampFieldCapsResponse, + inputIndices, + ruleStatusClient, + ruleId, + spaceId, + logger, + buildRuleMessage, + } = args; + if (!wroteStatus && isEmpty(timestampFieldCapsResponse.body.indices)) { const errorString = `This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ${JSON.stringify( inputIndices @@ -140,7 +170,12 @@ export const hasTimestampFields = async ( : '' }`; logger.error(buildRuleMessage(errorString.trimEnd())); - await ruleStatusService.partialFailure(errorString.trimEnd()); + await ruleStatusClient.logStatusChange({ + message: errorString.trimEnd(), + ruleId, + spaceId, + newStatus: RuleExecutionStatus['partial failure'], + }); return true; } else if ( !wroteStatus && @@ -161,7 +196,12 @@ export const hasTimestampFields = async ( : timestampFieldCapsResponse.body.fields[timestampField]?.unmapped?.indices )}`; logger.error(buildRuleMessage(errorString)); - await ruleStatusService.partialFailure(errorString); + await ruleStatusClient.logStatusChange({ + message: errorString, + ruleId, + spaceId, + newStatus: RuleExecutionStatus['partial failure'], + }); return true; } return wroteStatus;