From fddd9d799222a301cda6399e3a687406876de59f Mon Sep 17 00:00:00 2001 From: Dmitry Shevchenko Date: Tue, 3 Aug 2021 15:25:26 +0200 Subject: [PATCH] Implement RuleExecutionLog (#103463) --- .../src/enumeration/index.test.ts | 39 +++ .../src/enumeration/index.ts | 24 ++ .../src/index.ts | 7 +- x-pack/plugins/rule_registry/server/index.ts | 3 +- .../server/rule_data_plugin_service/index.ts | 10 + .../security_solution/common/constants.ts | 1 + .../schemas/common/schemas.ts | 32 ++- .../schemas/request/rule_schemas.ts | 4 +- .../schemas/response/rules_schema.mocks.ts | 3 +- .../schemas/response/rules_schema.ts | 4 +- .../common/detection_engine/utils.ts | 12 +- .../common/utils/invariant.ts | 23 ++ .../components/rules/rule_status/helpers.ts | 4 +- .../components/rules/rule_status/index.tsx | 4 +- .../detection_engine/rules/__mocks__/api.ts | 5 +- .../detection_engine/rules/types.ts | 21 +- .../routes/__mocks__/request_context.ts | 8 +- .../routes/__mocks__/request_responses.ts | 192 ++++++------- .../rules/add_prepackaged_rules_route.ts | 9 +- .../routes/rules/create_rules_route.test.ts | 4 +- .../routes/rules/create_rules_route.ts | 17 +- .../rules/delete_rules_bulk_route.test.ts | 6 +- .../routes/rules/delete_rules_bulk_route.ts | 9 +- .../routes/rules/delete_rules_route.test.ts | 6 +- .../routes/rules/delete_rules_route.ts | 11 +- .../routes/rules/find_rules_route.test.ts | 10 +- .../routes/rules/find_rules_route.ts | 10 +- .../rules/find_rules_status_route.test.ts | 4 +- .../routes/rules/find_rules_status_route.ts | 10 +- .../routes/rules/import_rules_route.ts | 4 +- .../routes/rules/patch_rules_bulk_route.ts | 14 +- .../routes/rules/patch_rules_route.test.ts | 7 +- .../routes/rules/patch_rules_route.ts | 20 +- .../rules/perform_bulk_action_route.test.ts | 2 - .../routes/rules/perform_bulk_action_route.ts | 16 +- .../routes/rules/read_rules_route.test.ts | 5 +- .../routes/rules/read_rules_route.ts | 16 +- .../rules/update_rules_bulk_route.test.ts | 2 - .../routes/rules/update_rules_bulk_route.ts | 14 +- .../routes/rules/update_rules_route.test.ts | 3 +- .../routes/rules/update_rules_route.ts | 20 +- .../routes/rules/validate.test.ts | 9 +- .../detection_engine/routes/rules/validate.ts | 14 +- .../lib/detection_engine/routes/utils.test.ts | 7 +- .../__mocks__/rule_execution_log_client.ts | 22 ++ .../adapters/rule_registry_dapter.ts | 105 ++++++++ .../adapters/saved_objects_adapter.ts | 63 +++++ .../rule_execution_log_client.ts | 69 +++++ .../rule_registry_log_client/constants.ts | 41 +++ .../parse_rule_execution_log.ts | 37 +++ .../rule_execution_field_map.ts | 32 +++ .../rule_execution_log_bootstrapper.ts | 48 ++++ .../rule_registry_log_client.ts | 252 ++++++++++++++++++ .../rule_registry_log_client/utils.ts | 76 ++++++ .../rule_execution_log/types.ts | 69 +++++ .../with_rule_execution_log.ts | 75 ++++++ .../rules/delete_rules.test.ts | 13 +- .../detection_engine/rules/delete_rules.ts | 2 +- .../lib/detection_engine/rules/enable_rule.ts | 41 +-- .../rules/patch_rules.mock.ts | 8 +- .../lib/detection_engine/rules/patch_rules.ts | 5 +- .../lib/detection_engine/rules/types.ts | 19 +- .../rules/update_prepacked_rules.test.ts | 9 +- .../rules/update_prepacked_rules.ts | 25 +- .../rules/update_rules.mock.ts | 12 +- .../detection_engine/rules/update_rules.ts | 5 +- .../schemas/rule_converters.ts | 3 +- .../signals/__mocks__/es_results.ts | 18 +- .../signals/build_signal.test.ts | 5 +- .../signals/get_or_create_rule_statuses.ts | 7 +- .../signals/get_rule_status_saved_objects.ts | 4 +- .../rule_status_saved_objects_client.ts | 14 +- .../signals/rule_status_service.test.ts | 15 +- .../signals/rule_status_service.ts | 22 +- .../security_solution/server/plugin.ts | 14 +- .../plugins/security_solution/server/types.ts | 3 + 76 files changed, 1383 insertions(+), 395 deletions(-) create mode 100644 packages/kbn-securitysolution-io-ts-types/src/enumeration/index.test.ts create mode 100644 packages/kbn-securitysolution-io-ts-types/src/enumeration/index.ts create mode 100644 x-pack/plugins/security_solution/common/utils/invariant.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/rule_registry_dapter.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/saved_objects_adapter.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/constants.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/parse_rule_execution_log.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_field_map.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_log_bootstrapper.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/utils.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/with_rule_execution_log.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/enumeration/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/enumeration/index.test.ts new file mode 100644 index 0000000000000..c904618689ca9 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/enumeration/index.test.ts @@ -0,0 +1,39 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { left } from 'fp-ts/lib/Either'; +import { enumeration } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('enumeration', () => { + enum TestEnum { + 'test' = 'test', + } + + it('should validate a string from the enum', () => { + const input = TestEnum.test; + const codec = enumeration('TestEnum', TestEnum); + const decoded = codec.decode(input); + const message = foldLeftRight(decoded); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(input); + }); + + it('should NOT validate a random string', () => { + const input = 'some string'; + const codec = enumeration('TestEnum', TestEnum); + const decoded = codec.decode(input); + const message = foldLeftRight(decoded); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "TestEnum"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-types/src/enumeration/index.ts b/packages/kbn-securitysolution-io-ts-types/src/enumeration/index.ts new file mode 100644 index 0000000000000..917d6d3bc6c01 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/enumeration/index.ts @@ -0,0 +1,24 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +export function enumeration( + name: string, + originalEnum: Record +): t.Type { + const isEnumValue = (input: unknown): input is EnumType => + Object.values(originalEnum).includes(input); + + return new t.Type( + name, + isEnumValue, + (input, context) => (isEnumValue(input) ? t.success(input) : t.failure(input, context)), + t.identity + ); +} diff --git a/packages/kbn-securitysolution-io-ts-types/src/index.ts b/packages/kbn-securitysolution-io-ts-types/src/index.ts index 2847894d63690..b85bff63fe2a7 100644 --- a/packages/kbn-securitysolution-io-ts-types/src/index.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/index.ts @@ -15,15 +15,16 @@ export * from './default_string_boolean_false'; export * from './default_uuid'; export * from './default_version_number'; export * from './empty_string_array'; +export * from './enumeration'; export * from './iso_date_string'; export * from './non_empty_array'; export * from './non_empty_or_nullable_string_array'; -export * from './non_empty_string'; export * from './non_empty_string_array'; -export * from './operator'; +export * from './non_empty_string'; export * from './only_false_allowed'; -export * from './positive_integer'; +export * from './operator'; export * from './positive_integer_greater_than_zero'; +export * from './positive_integer'; export * from './string_to_positive_number'; export * from './uuid'; export * from './version'; diff --git a/x-pack/plugins/rule_registry/server/index.ts b/x-pack/plugins/rule_registry/server/index.ts index f8d9dec3ea83a..e6656420af05d 100644 --- a/x-pack/plugins/rule_registry/server/index.ts +++ b/x-pack/plugins/rule_registry/server/index.ts @@ -18,13 +18,14 @@ export { createLifecycleRuleTypeFactory, LifecycleAlertService, } from './utils/create_lifecycle_rule_type_factory'; +export { RuleDataPluginService } from './rule_data_plugin_service'; export { LifecycleRuleExecutor, LifecycleAlertServices, createLifecycleExecutor, } from './utils/create_lifecycle_executor'; export { createPersistenceRuleTypeFactory } from './utils/create_persistence_rule_type_factory'; -export type { AlertTypeWithExecutor } from './types'; +export { AlertTypeWithExecutor } from './types'; export const plugin = (initContext: PluginInitializerContext) => new RuleRegistryPlugin(initContext); diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts index 6ca12042a47ce..f81340889e4b5 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts @@ -147,6 +147,12 @@ export class RuleDataPluginService { return; } catch (err) { if (err.meta?.body?.error?.type !== 'illegal_argument_exception') { + /** + * We skip the rollover if we catch anything except for illegal_argument_exception - that's the error + * returned by ES when the mapping update contains a conflicting field definition (e.g., a field changes types). + * We expect to get that error for some mapping changes we might make, and in those cases, + * we want to continue to rollover the index. Other errors are unexpected. + */ this.options.logger.error(`Failed to PUT mapping for alias ${alias}: ${err.message}`); return; } @@ -161,6 +167,10 @@ export class RuleDataPluginService { new_index: newIndexName, }); } catch (e) { + /** + * If we catch resource_already_exists_exception, that means that the index has been + * rolled over already — nothing to do for us in this case. + */ if (e?.meta?.body?.error?.type !== 'resource_already_exists_exception') { this.options.logger.error(`Failed to rollover index for alias ${alias}: ${e.message}`); } diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 4e2fca63bc51d..1b33518903f02 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -55,6 +55,7 @@ export const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000; // ms export const DEFAULT_RULE_REFRESH_IDLE_VALUE = 2700000; // ms export const DEFAULT_RULE_NOTIFICATION_QUERY_SIZE = 100; export const SAVED_OBJECTS_MANAGEMENT_FEATURE_ID = 'Saved Objects Management'; +export const DEFAULT_SPACE_ID = 'default'; // Document path where threat indicator fields are expected. Fields are used // to enrich signals, and are copied to threat.indicator. diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index c9a9d3bdcb24c..c869a12faf360 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -7,15 +7,15 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import * as t from 'io-ts'; - import { - UUID, - NonEmptyString, + enumeration, IsoDateString, - PositiveIntegerGreaterThanZero, + NonEmptyString, PositiveInteger, + PositiveIntegerGreaterThanZero, + UUID, } from '@kbn/securitysolution-io-ts-types'; +import * as t from 'io-ts'; export const author = t.array(t.string); export type Author = t.TypeOf; @@ -173,14 +173,18 @@ export type RuleNameOverrideOrUndefined = t.TypeOf; -export const job_status = t.keyof({ - succeeded: null, - failed: null, - 'going to run': null, - 'partial failure': null, - warning: null, -}); -export type JobStatus = t.TypeOf; +export enum RuleExecutionStatus { + 'succeeded' = 'succeeded', + 'failed' = 'failed', + 'going to run' = 'going to run', + 'partial failure' = 'partial failure', + /** + * @deprecated 'partial failure' status should be used instead + */ + 'warning' = 'warning', +} + +export const ruleExecutionStatus = enumeration('RuleExecutionStatus', RuleExecutionStatus); export const conflicts = t.keyof({ abort: null, proceed: null }); export type Conflicts = t.TypeOf; @@ -419,4 +423,4 @@ export enum BulkAction { 'duplicate' = 'duplicate', } -export const bulkAction = t.keyof(BulkAction); +export const bulkAction = enumeration('BulkAction', BulkAction); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index 74cc47904003d..b1361d8513c65 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -62,7 +62,7 @@ import { updated_by, created_at, created_by, - job_status, + ruleExecutionStatus, status_date, last_success_at, last_success_message, @@ -405,7 +405,7 @@ const responseRequiredFields = { created_by, }; const responseOptionalFields = { - status: job_status, + status: ruleExecutionStatus, status_date, last_success_at, last_success_message, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts index 730e2949d7a11..5bbad750d997d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts @@ -6,6 +6,7 @@ */ import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../constants'; +import { RuleExecutionStatus } from '../common/schemas'; import { getListArrayMock } from '../types/lists.mock'; import { RulesSchema } from './rules_schema'; @@ -60,7 +61,7 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchem type: 'query', threat: [], version: 1, - status: 'succeeded', + status: RuleExecutionStatus.succeeded, status_date: '2020-02-22T16:47:50.047Z', last_success_at: '2020-02-22T16:47:50.047Z', last_success_message: 'succeeded', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts index 0924588600d38..0efd6dc5067cb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts @@ -62,7 +62,7 @@ import { timeline_id, timeline_title, threshold, - job_status, + ruleExecutionStatus, status_date, last_success_at, last_success_message, @@ -164,7 +164,7 @@ export const partialRulesSchema = t.partial({ license, throttle, rule_name_override, - status: job_status, + status: ruleExecutionStatus, status_date, timestamp_override, last_success_at, diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index f3f2280c4b837..28749e0400bbb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -16,7 +16,7 @@ import type { import { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { hasLargeValueList } from '@kbn/securitysolution-list-utils'; -import { JobStatus, Threshold, ThresholdNormalized } from './schemas/common/schemas'; +import { RuleExecutionStatus, Threshold, ThresholdNormalized } from './schemas/common/schemas'; export const hasLargeValueItem = ( exceptionItems: Array @@ -64,5 +64,11 @@ export const normalizeThresholdObject = (threshold: Threshold): ThresholdNormali export const normalizeMachineLearningJobIds = (value: string | string[]): string[] => Array.isArray(value) ? value : [value]; -export const getRuleStatusText = (value: JobStatus | null | undefined): JobStatus | null => - value === 'partial failure' ? 'warning' : value != null ? value : null; +export const getRuleStatusText = ( + value: RuleExecutionStatus | null | undefined +): RuleExecutionStatus | null => + value === RuleExecutionStatus['partial failure'] + ? RuleExecutionStatus.warning + : value != null + ? value + : null; diff --git a/x-pack/plugins/security_solution/common/utils/invariant.ts b/x-pack/plugins/security_solution/common/utils/invariant.ts new file mode 100644 index 0000000000000..c18c1496afd7d --- /dev/null +++ b/x-pack/plugins/security_solution/common/utils/invariant.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +export class InvariantError extends Error { + name = 'Invariant violation'; +} + +/** + * Asserts that the provided condition is always true + * and throws an invariant violation error otherwise + * + * @param condition Condition to assert + * @param message Error message to throw if the condition is falsy + */ +export function invariant(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new InvariantError(message); + } +} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts index cca745659d2cc..13381b350761b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { RuleStatusType } from '../../../containers/detection_engine/rules'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; -export const getStatusColor = (status: RuleStatusType | string | null) => +export const getStatusColor = (status: RuleExecutionStatus | string | null) => status == null ? 'subdued' : status === 'succeeded' diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx index 42a6cb8bed1d7..5273cdd6d4271 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx @@ -8,16 +8,16 @@ import { EuiFlexItem, EuiHealth, EuiText } from '@elastic/eui'; import React, { memo } from 'react'; -import { RuleStatusType } from '../../../containers/detection_engine/rules'; import { FormattedDate } from '../../../../common/components/formatted_date'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { getStatusColor } from './helpers'; import * as i18n from './translations'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; interface RuleStatusProps { children: React.ReactNode | null | undefined; statusDate: string | null | undefined; - status: RuleStatusType | null | undefined; + status: RuleExecutionStatus | null | undefined; } const RuleStatusComponent: React.FC = ({ children, statusDate, status }) => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts index 7fe3dd9ccf1a0..90e972ef90f5b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts @@ -20,6 +20,7 @@ import { import { savedRuleMock, rulesMock } from '../mock'; import { getRulesSchemaMock } from '../../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { RulesSchema } from '../../../../../../common/detection_engine/schemas/response'; +import { RuleExecutionStatus } from '../../../../../../common/detection_engine/schemas/common/schemas'; export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise => Promise.resolve(getRulesSchemaMock()); @@ -60,7 +61,7 @@ export const getRuleStatusById = async ({ current_status: { alert_id: 'alertId', status_date: 'mm/dd/yyyyTHH:MM:sssz', - status: 'succeeded', + status: RuleExecutionStatus.succeeded, last_failure_at: null, last_success_at: 'mm/dd/yyyyTHH:MM:sssz', last_failure_message: null, @@ -86,7 +87,7 @@ export const getRulesStatusByIds = async ({ current_status: { alert_id: 'alertId', status_date: 'mm/dd/yyyyTHH:MM:sssz', - status: 'succeeded', + status: RuleExecutionStatus.succeeded, last_failure_at: null, last_success_at: 'mm/dd/yyyyTHH:MM:sssz', last_failure_message: null, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index 20bdeaf7e6378..9faed2d0646e0 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -29,6 +29,8 @@ import { timestamp_override, threshold, BulkAction, + ruleExecutionStatus, + RuleExecutionStatus, } from '../../../../../common/detection_engine/schemas/common/schemas'; import { CreateRulesSchema, @@ -75,14 +77,6 @@ const MetaRule = t.intersection([ }), ]); -const StatusTypes = t.union([ - t.literal('succeeded'), - t.literal('failed'), - t.literal('going to run'), - t.literal('partial failure'), - t.literal('warning'), -]); - // TODO: make a ticket export const RuleSchema = t.intersection([ t.type({ @@ -130,7 +124,7 @@ export const RuleSchema = t.intersection([ query: t.string, rule_name_override, saved_id: t.string, - status: StatusTypes, + status: ruleExecutionStatus, status_date: t.string, threshold, threat_query, @@ -274,17 +268,10 @@ export interface RuleStatus { current_status: RuleInfoStatus; failures: RuleInfoStatus[]; } - -export type RuleStatusType = - | 'failed' - | 'going to run' - | 'succeeded' - | 'partial failure' - | 'warning'; export interface RuleInfoStatus { alert_id: string; status_date: string; - status: RuleStatusType | null; + status: RuleExecutionStatus | null; last_failure_at: string | null; last_success_at: string | null; last_failure_message: string | null; 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 53c7f9d1fbb11..b6d6a8200aba1 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,12 +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'; const createMockClients = () => ({ rulesClient: rulesClientMock.create(), licensing: { license: licensingMock.createLicenseMock() }, clusterClient: elasticsearchServiceMock.createScopedClusterClient(), savedObjectsClient: savedObjectsClientMock.create(), + ruleExecutionLogClient: new RuleExecutionLogClient(), appClient: siemMock.createClient(), }); @@ -57,7 +59,11 @@ const createRequestContextMock = ( savedObjects: { client: clients.savedObjectsClient }, }, licensing: clients.licensing, - securitySolution: { getAppClient: jest.fn(() => clients.appClient) }, + securitySolution: { + getAppClient: jest.fn(() => clients.appClient), + getExecutionLogClient: jest.fn(() => clients.ruleExecutionLogClient), + getSpaceId: jest.fn(() => 'default'), + }, } as unknown) as SecuritySolutionRequestHandlerContextMock; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 8959c2b89d2b6..2f395117e8a0b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObjectsFindResponse } from 'kibana/server'; +import { SavedObjectsFindResponse, SavedObjectsFindResult } from 'kibana/server'; import { ActionResult } from '../../../../../../actions/server'; import { SignalSearchResponse } from '../../signals/types'; import { @@ -24,6 +24,7 @@ import { RuleAlertType, IRuleSavedAttributesSavedObjectAttributes, HapiReadableStream, + IRuleStatusSOAttributes, } from '../../rules/types'; import { requestMock } from './request'; import { RuleNotificationAlertType } from '../../notifications/types'; @@ -37,6 +38,8 @@ import { RuleParams } from '../../schemas/rule_schemas'; import { Alert } from '../../../../../../alerting/common'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; import { getPerformBulkActionSchemaMock } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { FindBulkExecutionLogResponse } from '../../rule_execution_log/types'; export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ signal_ids: ['somefakeid1', 'somefakeid2'], @@ -442,128 +445,93 @@ export const getMockPrivilegesResult = () => ({ application: {}, }); -export const getFindResultStatusEmpty = (): SavedObjectsFindResponse => ({ +export const getEmptySavedObjectsResponse = (): SavedObjectsFindResponse => ({ page: 1, per_page: 1, total: 0, saved_objects: [], }); -export const getFindResultStatus = (): SavedObjectsFindResponse => ({ - page: 1, - per_page: 6, - total: 2, - saved_objects: [ - { - type: 'my-type', - id: 'e0b86950-4e9f-11ea-bdbd-07b56aa159b3', - attributes: { - alertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bc', - statusDate: '2020-02-18T15:26:49.783Z', - status: 'succeeded', - lastFailureAt: undefined, - lastSuccessAt: '2020-02-18T15:26:49.783Z', - lastFailureMessage: undefined, - lastSuccessMessage: 'succeeded', - lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), - gap: '500.32', - searchAfterTimeDurations: ['200.00'], - bulkCreateTimeDurations: ['800.43'], - }, - score: 1, - references: [], - updated_at: '2020-02-18T15:26:51.333Z', - version: 'WzQ2LDFd', +export const getRuleExecutionStatuses = (): Array< + SavedObjectsFindResult +> => [ + { + type: 'my-type', + id: 'e0b86950-4e9f-11ea-bdbd-07b56aa159b3', + attributes: { + alertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bc', + statusDate: '2020-02-18T15:26:49.783Z', + status: RuleExecutionStatus.succeeded, + lastFailureAt: undefined, + lastSuccessAt: '2020-02-18T15:26:49.783Z', + lastFailureMessage: undefined, + lastSuccessMessage: 'succeeded', + lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), + gap: '500.32', + searchAfterTimeDurations: ['200.00'], + bulkCreateTimeDurations: ['800.43'], }, + score: 1, + references: [], + updated_at: '2020-02-18T15:26:51.333Z', + version: 'WzQ2LDFd', + }, + { + type: 'my-type', + id: '91246bd0-5261-11ea-9650-33b954270f67', + attributes: { + alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', + statusDate: '2020-02-18T15:15:58.806Z', + status: RuleExecutionStatus.failed, + lastFailureAt: '2020-02-18T15:15:58.806Z', + lastSuccessAt: '2020-02-13T20:31:59.855Z', + lastFailureMessage: + 'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.', + lastSuccessMessage: 'succeeded', + lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), + gap: '500.32', + searchAfterTimeDurations: ['200.00'], + bulkCreateTimeDurations: ['800.43'], + }, + score: 1, + references: [], + updated_at: '2020-02-18T15:15:58.860Z', + version: 'WzMyLDFd', + }, +]; + +export const getFindBulkResultStatus = (): FindBulkExecutionLogResponse => ({ + '04128c15-0d1b-4716-a4c5-46997ac7f3bd': [ { - type: 'my-type', - id: '91246bd0-5261-11ea-9650-33b954270f67', - attributes: { - alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', - statusDate: '2020-02-18T15:15:58.806Z', - status: 'failed', - lastFailureAt: '2020-02-18T15:15:58.806Z', - lastSuccessAt: '2020-02-13T20:31:59.855Z', - lastFailureMessage: - 'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.', - lastSuccessMessage: 'succeeded', - lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), - gap: '500.32', - searchAfterTimeDurations: ['200.00'], - bulkCreateTimeDurations: ['800.43'], - }, - score: 1, - references: [], - updated_at: '2020-02-18T15:15:58.860Z', - version: 'WzMyLDFd', + alertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + statusDate: '2020-02-18T15:26:49.783Z', + status: RuleExecutionStatus.succeeded, + lastFailureAt: undefined, + lastSuccessAt: '2020-02-18T15:26:49.783Z', + lastFailureMessage: undefined, + lastSuccessMessage: 'succeeded', + lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), + gap: '500.32', + searchAfterTimeDurations: ['200.00'], + bulkCreateTimeDurations: ['800.43'], }, ], -}); - -export const getFindBulkResultStatus = (): SavedObjectsFindResponse => ({ - page: 1, - per_page: 6, - total: 2, - saved_objects: [], - aggregations: { - alertIds: { - buckets: [ - { - key: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - most_recent_statuses: { - hits: { - hits: [ - { - _source: { - 'siem-detection-engine-rule-status': { - alertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - statusDate: '2020-02-18T15:26:49.783Z', - status: 'succeeded', - lastFailureAt: undefined, - lastSuccessAt: '2020-02-18T15:26:49.783Z', - lastFailureMessage: undefined, - lastSuccessMessage: 'succeeded', - lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), - gap: '500.32', - searchAfterTimeDurations: ['200.00'], - bulkCreateTimeDurations: ['800.43'], - }, - }, - }, - ], - }, - }, - }, - { - key: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', - most_recent_statuses: { - hits: { - hits: [ - { - _source: { - 'siem-detection-engine-rule-status': { - alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', - statusDate: '2020-02-18T15:15:58.806Z', - status: 'failed', - lastFailureAt: '2020-02-18T15:15:58.806Z', - lastSuccessAt: '2020-02-13T20:31:59.855Z', - lastFailureMessage: - 'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.', - lastSuccessMessage: 'succeeded', - lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), - gap: '500.32', - searchAfterTimeDurations: ['200.00'], - bulkCreateTimeDurations: ['800.43'], - }, - }, - }, - ], - }, - }, - }, - ], + '1ea5a820-4da1-4e82-92a1-2b43a7bece08': [ + { + alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', + statusDate: '2020-02-18T15:15:58.806Z', + status: RuleExecutionStatus.failed, + lastFailureAt: '2020-02-18T15:15:58.806Z', + lastSuccessAt: '2020-02-13T20:31:59.855Z', + lastFailureMessage: + 'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.', + lastSuccessMessage: 'succeeded', + lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), + gap: '500.32', + searchAfterTimeDurations: ['200.00'], + bulkCreateTimeDurations: ['800.43'], }, - }, + ], }); export const getEmptySignalsResponse = (): SignalSearchResponse => ({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 48a847474eeed..21933b2918722 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -116,6 +116,7 @@ export const createPrepackagedRules = async ( const exceptionsListClient = context.lists != null ? context.lists.getExceptionListClient() : exceptionsClient; const ruleAssetsClient = ruleAssetSavedObjectsClientFactory(savedObjectsClient); + const ruleStatusClient = context.securitySolution.getExecutionLogClient(); if (!siemClient || !rulesClient) { throw new PrepackagedRulesError('', 404); } @@ -154,7 +155,13 @@ export const createPrepackagedRules = async ( timeline, importTimelineResultSchema ); - await updatePrepackagedRules(rulesClient, savedObjectsClient, rulesToUpdate, signalsIndex); + await updatePrepackagedRules( + rulesClient, + context.securitySolution.getSpaceId(), + ruleStatusClient, + rulesToUpdate, + signalsIndex + ); const prepackagedRulesOutput: PrePackagedRulesAndTimelinesSchema = { rules_installed: rulesToInstall.length, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 25bb7f2bf0d0d..18767af066d27 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -10,7 +10,7 @@ import { getEmptyFindResult, getAlertMock, getCreateRequest, - getFindResultStatus, + getRuleExecutionStatuses, getFindResultWithSingleHit, createMlRuleRequest, } from '../__mocks__/request_responses'; @@ -38,7 +38,7 @@ describe('create_rules', () => { clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); // no current rules clients.rulesClient.create.mockResolvedValue(getAlertMock(getQueryRuleParams())); // creation succeeds - clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); // needed to transform + clients.ruleExecutionLogClient.find.mockResolvedValue(getRuleExecutionStatuses()); // needed to transform: ; context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } }) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index 6f4cf633e5fdd..6ada1e705a852 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -17,7 +17,6 @@ import { readRules } from '../../rules/read_rules'; import { buildSiemResponse } from '../utils'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; -import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { createRulesSchema } from '../../../../../common/detection_engine/schemas/request'; import { newTransformValidate } from './validate'; import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents'; @@ -106,18 +105,12 @@ export const createRulesRoute = ( name: createdRule.name, }); - const ruleStatuses = await ruleStatusSavedObjectsClientFactory(savedObjectsClient).find({ - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: `${createdRule.id}`, - searchFields: ['alertId'], + const ruleStatuses = await context.securitySolution.getExecutionLogClient().find({ + logsCount: 1, + ruleId: createdRule.id, + spaceId: context.securitySolution.getSpaceId(), }); - const [validated, errors] = newTransformValidate( - createdRule, - ruleActions, - ruleStatuses.saved_objects[0] - ); + const [validated, errors] = newTransformValidate(createdRule, ruleActions, ruleStatuses[0]); if (errors != null) { return siemResponse.error({ statusCode: 500, body: errors }); } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts index d37b0f5a685af..66feb3cae724f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts @@ -13,8 +13,7 @@ import { getDeleteBulkRequestById, getDeleteAsPostBulkRequest, getDeleteAsPostBulkRequestById, - getFindResultStatusEmpty, - getFindResultStatus, + getEmptySavedObjectsResponse, } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { deleteRulesBulkRoute } from './delete_rules_bulk_route'; @@ -29,7 +28,7 @@ describe('delete_rules', () => { clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists clients.rulesClient.delete.mockResolvedValue({}); // successful deletion - clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatusEmpty()); // rule status request + clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); // rule status request deleteRulesBulkRoute(server.router); }); @@ -41,7 +40,6 @@ describe('delete_rules', () => { }); test('resturns 200 when deleting a single rule and related rule status', async () => { - clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const response = await server.inject(getDeleteBulkRequest(), context); expect(response.status).toEqual(200); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 403565debea08..5016f93ef2cf5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -23,7 +23,6 @@ import { getIdBulkError } from './utils'; import { transformValidateBulkError } from './validate'; import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '../utils'; import { deleteRules } from '../../rules/delete_rules'; -import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { readRules } from '../../rules/read_rules'; type Config = RouteConfig; @@ -57,7 +56,7 @@ export const deleteRulesBulkRoute = (router: SecuritySolutionPluginRouter) => { return siemResponse.error({ statusCode: 404 }); } - const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const ruleStatusClient = context.securitySolution.getExecutionLogClient(); const rules = await Promise.all( request.body.map(async (payloadRule) => { @@ -79,9 +78,9 @@ export const deleteRulesBulkRoute = (router: SecuritySolutionPluginRouter) => { } const ruleStatuses = await ruleStatusClient.find({ - perPage: 6, - search: rule.id, - searchFields: ['alertId'], + logsCount: 6, + ruleId: rule.id, + spaceId: context.securitySolution.getSpaceId(), }); await deleteRules({ rulesClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index b64b14dc8cd0c..5102cb32a4572 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -12,7 +12,8 @@ import { getDeleteRequest, getFindResultWithSingleHit, getDeleteRequestById, - getFindResultStatus, + getRuleExecutionStatuses, + getEmptySavedObjectsResponse, } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { deleteRulesRoute } from './delete_rules_route'; @@ -27,7 +28,8 @@ describe('delete_rules', () => { ({ clients, context } = requestContextMock.createTools()); clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); - clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); + clients.ruleExecutionLogClient.find.mockResolvedValue(getRuleExecutionStatuses()); deleteRulesRoute(server.router); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts index 02c22750439f0..73d541802f055 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -19,7 +19,6 @@ import { deleteRules } from '../../rules/delete_rules'; import { getIdError, transform } from './utils'; import { buildSiemResponse } from '../utils'; -import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { readRules } from '../../rules/read_rules'; export const deleteRulesRoute = ( @@ -55,7 +54,7 @@ export const deleteRulesRoute = ( return siemResponse.error({ statusCode: 404 }); } - const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const ruleStatusClient = context.securitySolution.getExecutionLogClient(); const rule = await readRules({ rulesClient, id, ruleId }); if (!rule) { const error = getIdError({ id, ruleId }); @@ -66,9 +65,9 @@ export const deleteRulesRoute = ( } const ruleStatuses = await ruleStatusClient.find({ - perPage: 6, - search: rule.id, - searchFields: ['alertId'], + logsCount: 6, + ruleId: rule.id, + spaceId: context.securitySolution.getSpaceId(), }); await deleteRules({ rulesClient, @@ -77,7 +76,7 @@ export const deleteRulesRoute = ( ruleStatuses, id: rule.id, }); - const transformed = transform(rule, undefined, ruleStatuses.saved_objects[0]); + const transformed = transform(rule, undefined, ruleStatuses[0]); if (transformed == null) { return siemResponse.error({ statusCode: 500, body: 'failed to transform alert' }); } else { 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 ed92c045ade29..026c3fe973366 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 @@ -6,15 +6,16 @@ */ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; +import { requestContextMock, requestMock, serverMock } from '../__mocks__'; import { getAlertMock, + getFindBulkResultStatus, getFindRequest, + getEmptySavedObjectsResponse, getFindResultWithSingleHit, - getFindBulkResultStatus, } from '../__mocks__/request_responses'; -import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { findRulesRoute } from './find_rules_route'; -import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; jest.mock('../../signals/rule_status_service'); describe('find_rules', () => { @@ -27,7 +28,8 @@ describe('find_rules', () => { clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); - clients.savedObjectsClient.find.mockResolvedValue(getFindBulkResultStatus()); + clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); + clients.ruleExecutionLogClient.findBulk.mockResolvedValue(getFindBulkResultStatus()); findRulesRoute(server.router); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts index 32f18d816bacf..f0483c935f71c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -16,7 +16,6 @@ import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { findRules } from '../../rules/find_rules'; import { buildSiemResponse } from '../utils'; -import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { transformFindAlerts } from './utils'; import { getBulkRuleActionsSavedObject } from '../../rule_actions/get_bulk_rule_actions_saved_object'; @@ -53,7 +52,7 @@ export const findRulesRoute = ( return siemResponse.error({ statusCode: 404 }); } - const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const execLogClient = context.securitySolution.getExecutionLogClient(); const rules = await findRules({ rulesClient, perPage: query.per_page, @@ -64,8 +63,13 @@ export const findRulesRoute = ( fields: query.fields, }); const alertIds = rules.data.map((rule) => rule.id); + const [ruleStatuses, ruleActions] = await Promise.all([ - ruleStatusClient.findBulk(alertIds, 1), + execLogClient.findBulk({ + ruleIds: alertIds, + logsCount: 1, + spaceId: context.securitySolution.getSpaceId(), + }), getBulkRuleActionsSavedObject({ alertIds, savedObjectsClient }), ]); const transformed = transformFindAlerts(rules, ruleActions, ruleStatuses); 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 43e60b0eb5035..009c5ac56a009 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 @@ -26,7 +26,7 @@ describe('find_statuses', () => { beforeEach(async () => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - clients.savedObjectsClient.find.mockResolvedValue(getFindBulkResultStatus()); // successful status search + clients.ruleExecutionLogClient.findBulk.mockResolvedValue(getFindBulkResultStatus()); // successful status search clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); findRulesStatusesRoute(server.router); }); @@ -45,7 +45,7 @@ describe('find_statuses', () => { }); test('catch error when status search throws error', async () => { - clients.savedObjectsClient.find.mockImplementation(async () => { + clients.ruleExecutionLogClient.findBulk.mockImplementation(async () => { throw new Error('Test error'); }); const response = await server.inject(ruleStatusRequest(), context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index 1760d6bf7c18b..71ebe23f124d2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -10,7 +10,6 @@ import { buildRouteValidation } from '../../../../utils/build_validation/route_v import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { buildSiemResponse, mergeStatuses, getFailingRules } from '../utils'; -import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { findRulesStatusesSchema, FindRulesStatusesSchemaDecoded, @@ -41,7 +40,6 @@ export const findRulesStatusesRoute = (router: SecuritySolutionPluginRouter) => const { body } = request; const siemResponse = buildSiemResponse(response); const rulesClient = context.alerting?.getRulesClient(); - const savedObjectsClient = context.core.savedObjects.client; if (!rulesClient) { return siemResponse.error({ statusCode: 404 }); @@ -49,9 +47,13 @@ export const findRulesStatusesRoute = (router: SecuritySolutionPluginRouter) => const ids = body.ids; try { - const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const ruleStatusClient = context.securitySolution.getExecutionLogClient(); const [statusesById, failingRules] = await Promise.all([ - ruleStatusClient.findBulk(ids, 6), + ruleStatusClient.findBulk({ + ruleIds: ids, + logsCount: 6, + spaceId: context.securitySolution.getSpaceId(), + }), getFailingRules(ids, rulesClient), ]); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index b4a7fc4ac554f..2b9abd2088292 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -92,6 +92,7 @@ export const importRulesRoute = ( savedObjectsClient, }); + const ruleStatusClient = context.securitySolution.getExecutionLogClient(); const { filename } = (request.body.file as HapiReadableStream).hapi; const fileExtension = extname(filename).toLowerCase(); if (fileExtension !== '.ndjson') { @@ -259,7 +260,8 @@ export const importRulesRoute = ( rulesClient, author, buildingBlockType, - savedObjectsClient, + spaceId: context.securitySolution.getSpaceId(), + ruleStatusClient, description, enabled, eventCategoryOverride, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 6def864885dcd..d2b3396b64a2c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -23,7 +23,6 @@ import { getIdBulkError } from './utils'; import { transformValidateBulkError } from './validate'; import { patchRules } from '../../rules/patch_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; -import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { readRules } from '../../rules/read_rules'; import { PartialFilter } from '../../types'; @@ -47,6 +46,7 @@ export const patchRulesBulkRoute = ( const siemResponse = buildSiemResponse(response); const rulesClient = context.alerting?.getRulesClient(); + const ruleStatusClient = context.securitySolution.getExecutionLogClient(); const savedObjectsClient = context.core.savedObjects.client; if (!rulesClient) { @@ -59,7 +59,6 @@ export const patchRulesBulkRoute = ( request, savedObjectsClient, }); - const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await Promise.all( request.body.map(async (payloadRule) => { const { @@ -144,7 +143,8 @@ export const patchRulesBulkRoute = ( license, outputIndex, savedId, - savedObjectsClient, + spaceId: context.securitySolution.getSpaceId(), + ruleStatusClient, timelineId, timelineTitle, meta, @@ -190,11 +190,9 @@ export const patchRulesBulkRoute = ( name: rule.name, }); const ruleStatuses = await ruleStatusClient.find({ - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: rule.id, - searchFields: ['alertId'], + logsCount: 1, + ruleId: rule.id, + spaceId: context.securitySolution.getSpaceId(), }); return transformValidateBulkError(rule.id, rule, ruleActions, ruleStatuses); } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index 840763661f0bc..16d65d6482d21 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -10,12 +10,13 @@ import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../mach import { buildMlAuthz } from '../../../machine_learning/authz'; import { getEmptyFindResult, - getFindResultStatus, + getRuleExecutionStatuses, getAlertMock, getPatchRequest, getFindResultWithSingleHit, nonRuleFindResult, typicalMlRulePayload, + getEmptySavedObjectsResponse, } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { patchRulesRoute } from './patch_rules_route'; @@ -37,7 +38,9 @@ describe('patch_rules', () => { clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); // existing rule clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // existing rule clients.rulesClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams())); // successful update - clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); // successful transform + clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); // successful transform + clients.savedObjectsClient.create.mockResolvedValue(getRuleExecutionStatuses()[0]); // successful transform + clients.ruleExecutionLogClient.find.mockResolvedValue(getRuleExecutionStatuses()); patchRulesRoute(server.router, ml); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts index c6123a5ac53c8..45217fbd5e62c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -25,7 +25,6 @@ import { buildSiemResponse } from '../utils'; import { getIdError } from './utils'; import { transformValidate } from './validate'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; -import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { readRules } from '../../rules/read_rules'; import { PartialFilter } from '../../types'; @@ -108,6 +107,7 @@ export const patchRulesRoute = ( const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; const rulesClient = context.alerting?.getRulesClient(); + const ruleStatusClient = context.securitySolution.getExecutionLogClient(); const savedObjectsClient = context.core.savedObjects.client; if (!rulesClient) { @@ -131,7 +131,6 @@ export const patchRulesRoute = ( throwHttpError(await mlAuthz.validateRuleType(existingRule?.params.type)); } - const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rule = await patchRules({ rulesClient, author, @@ -146,7 +145,8 @@ export const patchRulesRoute = ( license, outputIndex, savedId, - savedObjectsClient, + spaceId: context.securitySolution.getSpaceId(), + ruleStatusClient, timelineId, timelineTitle, meta, @@ -193,18 +193,12 @@ export const patchRulesRoute = ( name: rule.name, }); const ruleStatuses = await ruleStatusClient.find({ - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: rule.id, - searchFields: ['alertId'], + logsCount: 1, + ruleId: rule.id, + spaceId: context.securitySolution.getSpaceId(), }); - const [validated, errors] = transformValidate( - rule, - ruleActions, - ruleStatuses.saved_objects[0] - ); + const [validated, errors] = transformValidate(rule, ruleActions, ruleStatuses[0]); if (errors != null) { return siemResponse.error({ statusCode: 500, body: errors }); } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts index 1facd291eede4..f8b3b834af857 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts @@ -10,7 +10,6 @@ import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../mach import { buildMlAuthz } from '../../../machine_learning/authz'; import { getEmptyFindResult, - getFindResultStatus, getBulkActionRequest, getFindResultWithSingleHit, getFindResultWithMultiHits, @@ -32,7 +31,6 @@ describe('perform_bulk_action', () => { ml = mlServicesMock.createSetupContract(); clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); - clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); performBulkActionRoute(server.router, ml); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts index ab6fc24c5fa76..0c4bdf0fcf64f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts @@ -21,7 +21,6 @@ import { findRules } from '../../rules/find_rules'; import { getExportByObjectIds } from '../../rules/get_export_by_object_ids'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; -import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { buildSiemResponse } from '../utils'; const BULK_ACTION_RULES_LIMIT = 10000; @@ -47,7 +46,7 @@ export const performBulkActionRoute = ( try { const rulesClient = context.alerting?.getRulesClient(); const savedObjectsClient = context.core.savedObjects.client; - const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const ruleStatusClient = context.securitySolution.getExecutionLogClient(); const mlAuthz = buildMlAuthz({ license: context.licensing.license, @@ -83,7 +82,12 @@ export const performBulkActionRoute = ( rules.data.map(async (rule) => { if (!rule.enabled) { throwHttpError(await mlAuthz.validateRuleType(rule.params.type)); - await enableRule({ rule, rulesClient, savedObjectsClient }); + await enableRule({ + rule, + rulesClient, + ruleStatusClient, + spaceId: context.securitySolution.getSpaceId(), + }); } }) ); @@ -102,9 +106,9 @@ export const performBulkActionRoute = ( await Promise.all( rules.data.map(async (rule) => { const ruleStatuses = await ruleStatusClient.find({ - perPage: 6, - search: rule.id, - searchFields: ['alertId'], + logsCount: 6, + ruleId: rule.id, + spaceId: context.securitySolution.getSpaceId(), }); await deleteRules({ rulesClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts index 11043273eaef0..586ff027425f8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts @@ -12,7 +12,7 @@ import { getReadRequest, getFindResultWithSingleHit, nonRuleFindResult, - getFindResultStatusEmpty, + getEmptySavedObjectsResponse, } from '../__mocks__/request_responses'; import { requestMock, requestContextMock, serverMock } from '../__mocks__'; @@ -25,7 +25,8 @@ describe('read_signals', () => { ({ clients, context } = requestContextMock.createTools()); clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists - clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatusEmpty()); // successful transform + clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); // successful transform + clients.ruleExecutionLogClient.find.mockResolvedValue([]); readRulesRoute(server.router); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts index ab618dc2a30e8..fc290190d86ee 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -20,7 +20,7 @@ import { buildSiemResponse } from '../utils'; import { readRules } from '../../rules/read_rules'; import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; -import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; export const readRulesRoute = ( router: SecuritySolutionPluginRouter, @@ -55,7 +55,7 @@ export const readRulesRoute = ( return siemResponse.error({ statusCode: 404 }); } - const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const ruleStatusClient = context.securitySolution.getExecutionLogClient(); const rule = await readRules({ rulesClient, id, @@ -67,18 +67,16 @@ export const readRulesRoute = ( ruleAlertId: rule.id, }); const ruleStatuses = await ruleStatusClient.find({ - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: rule.id, - searchFields: ['alertId'], + logsCount: 1, + ruleId: rule.id, + spaceId: context.securitySolution.getSpaceId(), }); - const [currentStatus] = ruleStatuses.saved_objects; + const [currentStatus] = ruleStatuses; if (currentStatus != null && rule.executionStatus.status === 'error') { currentStatus.attributes.lastFailureMessage = `Reason: ${rule.executionStatus.error?.reason} Message: ${rule.executionStatus.error?.message}`; currentStatus.attributes.lastFailureAt = rule.executionStatus.lastExecutionDate.toISOString(); currentStatus.attributes.statusDate = rule.executionStatus.lastExecutionDate.toISOString(); - currentStatus.attributes.status = 'failed'; + currentStatus.attributes.status = RuleExecutionStatus.failed; } const transformed = transform(rule, ruleActions, currentStatus); if (transformed == null) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index 7851920adc1d9..eeb8b3caf6df5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -13,7 +13,6 @@ import { getAlertMock, getFindResultWithSingleHit, getUpdateBulkRequest, - getFindResultStatus, typicalMlRulePayload, } from '../__mocks__/request_responses'; import { serverMock, requestContextMock, requestMock } from '../__mocks__'; @@ -36,7 +35,6 @@ describe('update_rules_bulk', () => { clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.rulesClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams())); - clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); updateRulesBulkRoute(server.router, ml); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index f7604ebcdc22f..44c9ce51b7a1e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -20,7 +20,6 @@ import { transformValidateBulkError } from './validate'; import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '../utils'; import { updateRules } from '../../rules/update_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; -import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const updateRulesBulkRoute = ( router: SecuritySolutionPluginRouter, @@ -54,7 +53,7 @@ export const updateRulesBulkRoute = ( savedObjectsClient, }); - const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const ruleStatusClient = context.securitySolution.getExecutionLogClient(); const rules = await Promise.all( request.body.map(async (payloadRule) => { const idOrRuleIdOrUnknown = payloadRule.id ?? payloadRule.rule_id ?? '(unknown id)'; @@ -71,8 +70,9 @@ export const updateRulesBulkRoute = ( throwHttpError(await mlAuthz.validateRuleType(payloadRule.type)); const rule = await updateRules({ + spaceId: context.securitySolution.getSpaceId(), rulesClient, - savedObjectsClient, + ruleStatusClient, defaultOutputIndex: siemClient.getSignalsIndex(), ruleUpdate: payloadRule, }); @@ -87,11 +87,9 @@ export const updateRulesBulkRoute = ( name: payloadRule.name, }); const ruleStatuses = await ruleStatusClient.find({ - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: rule.id, - searchFields: ['alertId'], + logsCount: 1, + ruleId: rule.id, + spaceId: context.securitySolution.getSpaceId(), }); return transformValidateBulkError(rule.id, rule, ruleActions, ruleStatuses); } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index 60a91521bc766..129e4bd8ad9a1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -12,7 +12,6 @@ import { getAlertMock, getUpdateRequest, getFindResultWithSingleHit, - getFindResultStatusEmpty, nonRuleFindResult, typicalMlRulePayload, } from '../__mocks__/request_responses'; @@ -39,7 +38,7 @@ describe('update_rules', () => { clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); // existing rule clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists clients.rulesClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams())); // successful update - clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatusEmpty()); // successful transform + clients.ruleExecutionLogClient.find.mockResolvedValue([]); // successful transform: ; updateRulesRoute(server.router, ml); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts index a6f07c2f84d16..23449227f6c70 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -20,7 +20,6 @@ import { getIdError } from './utils'; import { transformValidate } from './validate'; import { updateRules } from '../../rules/update_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; -import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; export const updateRulesRoute = ( @@ -48,7 +47,6 @@ export const updateRulesRoute = ( const rulesClient = context.alerting?.getRulesClient(); const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.securitySolution?.getAppClient(); - const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); if (!siemClient || !rulesClient) { return siemResponse.error({ statusCode: 404 }); @@ -62,9 +60,11 @@ export const updateRulesRoute = ( }); throwHttpError(await mlAuthz.validateRuleType(request.body.type)); + const ruleStatusClient = context.securitySolution.getExecutionLogClient(); const rule = await updateRules({ + spaceId: context.securitySolution.getSpaceId(), rulesClient, - savedObjectsClient, + ruleStatusClient, defaultOutputIndex: siemClient.getSignalsIndex(), ruleUpdate: request.body, }); @@ -80,17 +80,11 @@ export const updateRulesRoute = ( name: request.body.name, }); const ruleStatuses = await ruleStatusClient.find({ - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: rule.id, - searchFields: ['alertId'], + logsCount: 1, + ruleId: rule.id, + spaceId: context.securitySolution.getSpaceId(), }); - const [validated, errors] = transformValidate( - rule, - ruleActions, - ruleStatuses.saved_objects[0] - ); + const [validated, errors] = transformValidate(rule, ruleActions, ruleStatuses[0]); if (errors != null) { return siemResponse.error({ statusCode: 500, body: errors }); } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts index f971a5606f6c6..1ca8c27995922 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts @@ -8,10 +8,11 @@ import { transformValidate, transformValidateBulkError } from './validate'; import { BulkError } from '../utils'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; -import { getAlertMock, getFindResultStatus } from '../__mocks__/request_responses'; +import { getAlertMock, getRuleExecutionStatuses } from '../__mocks__/request_responses'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; export const ruleOutput = (): RulesSchema => ({ actions: [], @@ -107,12 +108,12 @@ describe('validate', () => { }); test('it should do a validation correctly of a rule id with ruleStatus passed in', () => { - const ruleStatus = getFindResultStatus(); + const ruleStatuses = getRuleExecutionStatuses(); const ruleAlert = getAlertMock(getQueryRuleParams()); - const validatedOrError = transformValidateBulkError('rule-1', ruleAlert, null, ruleStatus); + const validatedOrError = transformValidateBulkError('rule-1', ruleAlert, null, ruleStatuses); const expected: RulesSchema = { ...ruleOutput(), - status: 'succeeded', + status: RuleExecutionStatus.succeeded, status_date: '2020-02-18T15:26:49.783Z', last_success_at: '2020-02-18T15:26:49.783Z', last_success_message: 'succeeded', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts index d27208de487df..e3e2b8cda98b2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; +import { SavedObject, SavedObjectsFindResult } from 'kibana/server'; import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; import { @@ -20,8 +20,8 @@ import { PartialAlert } from '../../../../../../alerting/server'; import { isAlertType, IRuleSavedAttributesSavedObjectAttributes, - isRuleStatusFindType, IRuleStatusSOAttributes, + isRuleStatusSavedObjectType, } from '../../rules/types'; import { createBulkErrorObject, BulkError } from '../utils'; import { transform, transformAlertToRule } from './utils'; @@ -58,15 +58,11 @@ export const transformValidateBulkError = ( ruleId: string, alert: PartialAlert, ruleActions?: RuleActions | null, - ruleStatus?: SavedObjectsFindResponse + ruleStatus?: Array> ): RulesSchema | BulkError => { if (isAlertType(alert)) { - if (isRuleStatusFindType(ruleStatus) && ruleStatus?.saved_objects.length > 0) { - const transformed = transformAlertToRule( - alert, - ruleActions, - ruleStatus?.saved_objects[0] ?? ruleStatus - ); + if (ruleStatus && ruleStatus?.length > 0 && isRuleStatusSavedObjectType(ruleStatus[0])) { + const transformed = transformAlertToRule(alert, ruleActions, ruleStatus[0]); const [validated, errors] = validateNonExact(transformed, rulesSchema); if (errors != null || validated == null) { return createBulkErrorObject({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts index 83091c7e1f82e..c7e1f9f2e6bd7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts @@ -29,6 +29,7 @@ import { exampleRuleStatus } from '../signals/__mocks__/es_results'; import { getAlertMock } from './__mocks__/request_responses'; import { AlertExecutionStatusErrorReasons } from '../../../../../alerting/common'; import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; let rulesClient: ReturnType; @@ -297,9 +298,9 @@ describe('utils', () => { describe('mergeStatuses', () => { it('merges statuses and converts from camelCase saved object to snake_case HTTP response', () => { const statusOne = exampleRuleStatus(); - statusOne.attributes.status = 'failed'; + statusOne.attributes.status = RuleExecutionStatus.failed; const statusTwo = exampleRuleStatus(); - statusTwo.attributes.status = 'failed'; + statusTwo.attributes.status = RuleExecutionStatus.failed; const currentStatus = exampleRuleStatus(); const foundRules = [currentStatus.attributes, statusOne.attributes, statusTwo.attributes]; const res = mergeStatuses(currentStatus.attributes.alertId, foundRules, { @@ -307,7 +308,7 @@ describe('utils', () => { current_status: { alert_id: 'myfakealertid-8cfac', status_date: '2020-03-27T22:55:59.517Z', - status: 'succeeded', + status: RuleExecutionStatus.succeeded, last_failure_at: null, last_success_at: '2020-03-27T22:55:59.517Z', last_failure_message: null, 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 new file mode 100644 index 0000000000000..475b83a6a29cc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts @@ -0,0 +1,22 @@ +/* + * 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 { IRuleExecutionLogClient } from '../types'; + +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(), + }; + }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/rule_registry_dapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/rule_registry_dapter.ts new file mode 100644 index 0000000000000..3f56b26d32a09 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/rule_registry_dapter.ts @@ -0,0 +1,105 @@ +/* + * 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 { merge } from 'lodash'; +import { RuleDataPluginService } from '../../../../../../rule_registry/server'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { IRuleStatusSOAttributes } from '../../rules/types'; +import { RuleRegistryLogClient } from '../rule_registry_log_client/rule_registry_log_client'; +import { + ExecutionMetric, + ExecutionMetricArgs, + FindBulkExecutionLogArgs, + FindExecutionLogArgs, + IRuleExecutionLogClient, + LogStatusChangeArgs, +} from '../types'; + +/** + * @deprecated RuleRegistryAdapter is kept here only as a reference. It will be superseded with EventLog implementation + */ +export class RuleRegistryAdapter implements IRuleExecutionLogClient { + private ruleRegistryClient: RuleRegistryLogClient; + + constructor(ruleDataService: RuleDataPluginService) { + this.ruleRegistryClient = new RuleRegistryLogClient(ruleDataService); + } + + public async find({ ruleId, logsCount = 1, spaceId }: FindExecutionLogArgs) { + const logs = await this.ruleRegistryClient.find({ + ruleIds: [ruleId], + logsCount, + spaceId, + }); + + return logs[ruleId].map((log) => ({ + id: '', + type: '', + score: 0, + attributes: log, + references: [], + })); + } + + public async findBulk({ ruleIds, logsCount = 1, spaceId }: FindBulkExecutionLogArgs) { + const [statusesById, lastErrorsById] = await Promise.all([ + this.ruleRegistryClient.find({ ruleIds, spaceId }), + this.ruleRegistryClient.find({ + ruleIds, + statuses: [RuleExecutionStatus.failed], + logsCount, + spaceId, + }), + ]); + return merge(statusesById, lastErrorsById); + } + + public async create(event: IRuleStatusSOAttributes, spaceId: string) { + if (event.status) { + await this.ruleRegistryClient.logStatusChange({ + ruleId: event.alertId, + newStatus: event.status, + spaceId, + }); + } + + if (event.bulkCreateTimeDurations) { + await this.ruleRegistryClient.logExecutionMetric({ + ruleId: event.alertId, + metric: ExecutionMetric.indexingDurationMax, + value: Math.max(...event.bulkCreateTimeDurations.map(Number)), + spaceId, + }); + } + + if (event.gap) { + await this.ruleRegistryClient.logExecutionMetric({ + ruleId: event.alertId, + metric: ExecutionMetric.executionGap, + value: Number(event.gap), + spaceId, + }); + } + } + + public async update(id: string, event: IRuleStatusSOAttributes, spaceId: string) { + // execution events are immutable, so we just use 'create' here instead of 'update' + await this.create(event, spaceId); + } + + public async delete(id: string) { + // execution events are immutable, nothing to do here + } + + public async logExecutionMetric(args: ExecutionMetricArgs) { + return this.ruleRegistryClient.logExecutionMetric(args); + } + + public async logStatusChange(args: LogStatusChangeArgs) { + return this.ruleRegistryClient.logStatusChange(args); + } +} 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 new file mode 100644 index 0000000000000..55f65caf34b03 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/saved_objects_adapter.ts @@ -0,0 +1,63 @@ +/* + * 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 { IRuleStatusSOAttributes } from '../../rules/types'; +import { + RuleStatusSavedObjectsClient, + ruleStatusSavedObjectsClientFactory, +} from '../../signals/rule_status_saved_objects_client'; +import { + ExecutionMetric, + ExecutionMetricArgs, + FindBulkExecutionLogArgs, + FindExecutionLogArgs, + IRuleExecutionLogClient, + LogStatusChangeArgs, +} 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(event: IRuleStatusSOAttributes) { + await this.ruleStatusClient.create(event); + } + + public async update(id: string, event: IRuleStatusSOAttributes) { + await this.ruleStatusClient.update(id, event); + } + + 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 new file mode 100644 index 0000000000000..286238b292cb7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts @@ -0,0 +1,69 @@ +/* + * 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 { RuleDataPluginService } from '../../../../../rule_registry/server'; +import { IRuleStatusSOAttributes } from '../rules/types'; +import { RuleRegistryAdapter } from './adapters/rule_registry_dapter'; +import { SavedObjectsAdapter } from './adapters/saved_objects_adapter'; +import { + ExecutionMetric, + ExecutionMetricArgs, + FindBulkExecutionLogArgs, + FindExecutionLogArgs, + IRuleExecutionLogClient, + LogStatusChangeArgs, +} from './types'; + +export interface RuleExecutionLogClientArgs { + ruleDataService: RuleDataPluginService; + savedObjectsClient: SavedObjectsClientContract; +} + +const RULE_REGISTRY_LOG_ENABLED = false; + +export class RuleExecutionLogClient implements IRuleExecutionLogClient { + private client: IRuleExecutionLogClient; + + constructor({ ruleDataService, savedObjectsClient }: RuleExecutionLogClientArgs) { + if (RULE_REGISTRY_LOG_ENABLED) { + this.client = new RuleRegistryAdapter(ruleDataService); + } else { + this.client = new SavedObjectsAdapter(savedObjectsClient); + } + } + + public find(args: FindExecutionLogArgs) { + return this.client.find(args); + } + + public findBulk(args: FindBulkExecutionLogArgs) { + return this.client.findBulk(args); + } + + // TODO args as an object + public async create(event: IRuleStatusSOAttributes, spaceId: string) { + return this.client.create(event, spaceId); + } + + // TODO args as an object + public async update(id: string, event: IRuleStatusSOAttributes, spaceId: string) { + return this.client.update(id, event, spaceId); + } + + public async delete(id: string) { + return this.client.delete(id); + } + + public async logExecutionMetric(args: ExecutionMetricArgs) { + return this.client.logExecutionMetric(args); + } + + public async logStatusChange(args: LogStatusChangeArgs) { + return this.client.logStatusChange(args); + } +} 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_log_client/constants.ts new file mode 100644 index 0000000000000..8d74c71bf447d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/constants.ts @@ -0,0 +1,41 @@ +/* + * 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. + */ + +/** + * @deprecated EVENTS_INDEX_PREFIX is kept here only as a reference. It will be superseded with EventLog implementation + */ +export const EVENTS_INDEX_PREFIX = '.kibana_alerts-security.events'; + +/** + * @deprecated MESSAGE is kept here only as a reference. It will be superseded with EventLog implementation + */ +export const MESSAGE = 'message' as const; + +/** + * @deprecated EVENT_SEQUENCE is kept here only as a reference. It will be superseded with EventLog implementation + */ +export const EVENT_SEQUENCE = 'event.sequence' as const; + +/** + * @deprecated EVENT_DURATION is kept here only as a reference. It will be superseded with EventLog implementation + */ +export const EVENT_DURATION = 'event.duration' as const; + +/** + * @deprecated EVENT_END is kept here only as a reference. It will be superseded with EventLog implementation + */ +export const EVENT_END = 'event.end' as const; + +/** + * @deprecated RULE_STATUS is kept here only as a reference. It will be superseded with EventLog implementation + */ +export const RULE_STATUS = 'kibana.rac.detection_engine.rule_status' as const; + +/** + * @deprecated RULE_STATUS_SEVERITY is kept here only as a reference. It will be superseded with EventLog implementation + */ +export const RULE_STATUS_SEVERITY = 'kibana.rac.detection_engine.rule_status_severity' as const; 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_log_client/parse_rule_execution_log.ts new file mode 100644 index 0000000000000..ed556e312c5df --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/parse_rule_execution_log.ts @@ -0,0 +1,37 @@ +/* + * 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 { 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 { + mergeFieldMaps, + runtimeTypeFromFieldMap, +} from '../../../../../../rule_registry/common/field_map'; +import { ruleExecutionFieldMap } from './rule_execution_field_map'; + +const ruleExecutionLogRuntimeType = runtimeTypeFromFieldMap( + mergeFieldMaps(technicalRuleFieldMap, ruleExecutionFieldMap) +); + +/** + * @deprecated parseRuleExecutionLog is kept here only as a reference. It will be superseded with EventLog implementation + */ +export const parseRuleExecutionLog = (input: unknown) => { + const validate = ruleExecutionLogRuntimeType.decode(input); + + if (isLeft(validate)) { + throw new Error(PathReporter.report(validate).join('\n')); + } + + return ruleExecutionLogRuntimeType.encode(validate.right); +}; + +/** + * @deprecated RuleExecutionEvent is kept here only as a reference. It will be superseded with EventLog implementation + */ +export type RuleExecutionEvent = ReturnType; 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_log_client/rule_execution_field_map.ts new file mode 100644 index 0000000000000..b3c70cd56d9e6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_field_map.ts @@ -0,0 +1,32 @@ +/* + * 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 { + EVENT_DURATION, + EVENT_END, + EVENT_SEQUENCE, + MESSAGE, + RULE_STATUS, + RULE_STATUS_SEVERITY, +} from './constants'; + +/** + * @deprecated ruleExecutionFieldMap is kept here only as a reference. It will be superseded with EventLog implementation + */ +export const ruleExecutionFieldMap = { + [MESSAGE]: { type: 'keyword' }, + [EVENT_SEQUENCE]: { type: 'long' }, + [EVENT_END]: { type: 'date' }, + [EVENT_DURATION]: { type: 'long' }, + [RULE_STATUS]: { type: 'keyword' }, + [RULE_STATUS_SEVERITY]: { type: 'integer' }, +} as const; + +/** + * @deprecated RuleExecutionFieldMap is kept here only as a reference. It will be superseded with EventLog implementation + */ +export type RuleExecutionFieldMap = typeof ruleExecutionFieldMap; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_log_bootstrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_log_bootstrapper.ts new file mode 100644 index 0000000000000..50a9b45485741 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_log_bootstrapper.ts @@ -0,0 +1,48 @@ +/* + * 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 { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../../../../../rule_registry/common/assets'; +import { mappingFromFieldMap } from '../../../../../../rule_registry/common/mapping_from_field_map'; +import { RuleDataPluginService } from '../../../../../../rule_registry/server'; +import { ruleExecutionFieldMap } from './rule_execution_field_map'; + +/** + * @deprecated bootstrapRuleExecutionLog is kept here only as a reference. It will be superseded with EventLog implementation + */ +export const bootstrapRuleExecutionLog = async ( + ruleDataService: RuleDataPluginService, + indexAlias: string +) => { + const indexPattern = `${indexAlias}*`; + const componentTemplateName = `${indexAlias}-mappings`; + const indexTemplateName = `${indexAlias}-template`; + + await ruleDataService.createOrUpdateComponentTemplate({ + name: componentTemplateName, + body: { + template: { + settings: { + number_of_shards: 1, + }, + mappings: mappingFromFieldMap(ruleExecutionFieldMap), + }, + }, + }); + + await ruleDataService.createOrUpdateIndexTemplate({ + name: indexTemplateName, + body: { + index_patterns: [indexPattern], + composed_of: [ + ruleDataService.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME), + componentTemplateName, + ], + }, + }); + + await ruleDataService.updateIndexMappingsMatchingPattern(indexPattern); +}; 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_log_client/rule_registry_log_client.ts new file mode 100644 index 0000000000000..5094f9a8c6e3c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts @@ -0,0 +1,252 @@ +/* + * 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 { estypes } from '@elastic/elasticsearch'; +import { EVENT_ACTION, EVENT_KIND, RULE_ID, SPACE_IDS, TIMESTAMP } from '@kbn/rule-data-utils'; +import { once } from 'lodash/fp'; +import moment from 'moment'; +import { RuleDataClient, RuleDataPluginService } 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, LogStatusChangeArgs } from '../types'; +import { + EVENTS_INDEX_PREFIX, + MESSAGE, + EVENT_SEQUENCE, + RULE_STATUS, + RULE_STATUS_SEVERITY, +} from './constants'; +import { parseRuleExecutionLog, RuleExecutionEvent } from './parse_rule_execution_log'; +import { bootstrapRuleExecutionLog } from './rule_execution_log_bootstrapper'; +import { + getLastEntryAggregation, + getMetricAggregation, + getMetricField, + sortByTimeDesc, +} from './utils'; + +const statusSeverityDict: Record = { + [RuleExecutionStatus.succeeded]: 0, + [RuleExecutionStatus['going to run']]: 10, + [RuleExecutionStatus.warning]: 20, + [RuleExecutionStatus['partial failure']]: 20, + [RuleExecutionStatus.failed]: 30, +}; + +interface FindExecutionLogArgs { + ruleIds: string[]; + spaceId: string; + logsCount?: number; + statuses?: RuleExecutionStatus[]; +} + +interface IRuleRegistryLogClient { + find: ( + args: FindExecutionLogArgs + ) => Promise<{ + [ruleId: string]: IRuleStatusSOAttributes[] | undefined; + }>; + create: (event: RuleExecutionEvent) => Promise; + logStatusChange: (args: LogStatusChangeArgs) => Promise; + logExecutionMetric: (args: ExecutionMetricArgs) => Promise; +} + +/** + * @deprecated RuleRegistryLogClient is kept here only as a reference. It will be superseded with EventLog implementation + */ +export class RuleRegistryLogClient implements IRuleRegistryLogClient { + private sequence = 0; + private ruleDataClient: RuleDataClient; + + constructor(ruleDataService: RuleDataPluginService) { + this.ruleDataClient = ruleDataService.getRuleDataClient( + SERVER_APP_ID, + EVENTS_INDEX_PREFIX, + () => this.initialize(ruleDataService, EVENTS_INDEX_PREFIX) + ); + } + + private initialize = once(async (ruleDataService: RuleDataPluginService, indexAlias: string) => { + await bootstrapRuleExecutionLog(ruleDataService, indexAlias); + }); + + public async find({ ruleIds, spaceId, statuses, logsCount = 1 }: FindExecutionLogArgs) { + if (ruleIds.length === 0) { + return {}; + } + + const filter: estypes.QueryDslQueryContainer[] = [ + { terms: { [RULE_ID]: ruleIds } }, + { terms: { [SPACE_IDS]: [spaceId] } }, + ]; + + if (statuses) { + filter.push({ terms: { [RULE_STATUS]: statuses } }); + } + + const result = await this.ruleDataClient.getReader().search({ + size: 0, + body: { + query: { + bool: { + filter, + }, + }, + aggs: { + rules: { + terms: { + field: RULE_ID, + size: ruleIds.length, + }, + aggs: { + most_recent_logs: { + top_hits: { + sort: sortByTimeDesc, + size: logsCount, + }, + }, + last_failure: getLastEntryAggregation(RuleExecutionStatus.failed), + last_success: getLastEntryAggregation(RuleExecutionStatus.succeeded), + execution_gap: getMetricAggregation(ExecutionMetric.executionGap), + search_duration_max: getMetricAggregation(ExecutionMetric.searchDurationMax), + indexing_duration_max: getMetricAggregation(ExecutionMetric.indexingDurationMax), + indexing_lookback: getMetricAggregation(ExecutionMetric.indexingLookback), + }, + }, + }, + }, + }); + + if (result.hits.total.value === 0) { + return {}; + } + + invariant(result.aggregations, 'Search response should contain aggregations'); + + return Object.fromEntries( + result.aggregations.rules.buckets.map((bucket) => [ + bucket.key, + bucket.most_recent_logs.hits.hits.map((event) => { + const logEntry = parseRuleExecutionLog(event._source); + invariant(logEntry['rule.id'], 'Malformed execution log entry: rule.id field not found'); + + const lastFailure = bucket.last_failure.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.last_failure.event.hits.hits[0]._source) + : undefined; + + const lastSuccess = bucket.last_success.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.last_success.event.hits.hits[0]._source) + : undefined; + + const lookBack = bucket.indexing_lookback.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.indexing_lookback.event.hits.hits[0]._source) + : undefined; + + const executionGap = bucket.execution_gap.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.execution_gap.event.hits.hits[0]._source)[ + getMetricField(ExecutionMetric.executionGap) + ] + : undefined; + + const searchDuration = bucket.search_duration_max.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.search_duration_max.event.hits.hits[0]._source)[ + getMetricField(ExecutionMetric.searchDurationMax) + ] + : undefined; + + const indexingDuration = bucket.indexing_duration_max.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.indexing_duration_max.event.hits.hits[0]._source)[ + getMetricField(ExecutionMetric.indexingDurationMax) + ] + : undefined; + + const alertId = logEntry['rule.id']; + const statusDate = logEntry[TIMESTAMP]; + const lastFailureAt = lastFailure?.[TIMESTAMP]; + const lastFailureMessage = lastFailure?.[MESSAGE]; + const lastSuccessAt = lastSuccess?.[TIMESTAMP]; + const lastSuccessMessage = lastSuccess?.[MESSAGE]; + const status = (logEntry[RULE_STATUS] as RuleExecutionStatus) || null; + const lastLookBackDate = lookBack?.[getMetricField(ExecutionMetric.indexingLookback)]; + const gap = executionGap ? moment.duration(executionGap).humanize() : null; + const bulkCreateTimeDurations = indexingDuration + ? [makeFloatString(indexingDuration)] + : null; + const searchAfterTimeDurations = searchDuration + ? [makeFloatString(searchDuration)] + : null; + + return { + alertId, + statusDate, + lastFailureAt, + lastFailureMessage, + lastSuccessAt, + lastSuccessMessage, + status, + lastLookBackDate, + gap, + bulkCreateTimeDurations, + searchAfterTimeDurations, + }; + }), + ]) + ); + } + + public async logExecutionMetric({ + ruleId, + namespace, + metric, + value, + spaceId, + }: ExecutionMetricArgs) { + await this.create( + { + [SPACE_IDS]: [spaceId], + [EVENT_ACTION]: metric, + [EVENT_KIND]: 'metric', + [getMetricField(metric)]: value, + [RULE_ID]: ruleId, + [TIMESTAMP]: new Date().toISOString(), + }, + namespace + ); + } + + public async logStatusChange({ + ruleId, + newStatus, + namespace, + message, + spaceId, + }: LogStatusChangeArgs) { + await this.create( + { + [SPACE_IDS]: [spaceId], + [EVENT_ACTION]: 'status-change', + [EVENT_KIND]: 'event', + [EVENT_SEQUENCE]: this.sequence++, + [MESSAGE]: message, + [RULE_ID]: ruleId, + [RULE_STATUS_SEVERITY]: statusSeverityDict[newStatus], + [RULE_STATUS]: newStatus, + [TIMESTAMP]: new Date().toISOString(), + }, + namespace + ); + } + + public async create(event: RuleExecutionEvent, namespace?: string) { + await this.ruleDataClient.getWriter({ namespace }).bulk({ + body: [{ index: {} }, event], + }); + } +} 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_log_client/utils.ts new file mode 100644 index 0000000000000..4efbaa91dbda4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/utils.ts @@ -0,0 +1,76 @@ +/* + * 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 { 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 { RULE_STATUS, EVENT_SEQUENCE, EVENT_DURATION, EVENT_END } from './constants'; + +const METRIC_FIELDS = { + [ExecutionMetric.executionGap]: EVENT_DURATION, + [ExecutionMetric.searchDurationMax]: EVENT_DURATION, + [ExecutionMetric.indexingDurationMax]: EVENT_DURATION, + [ExecutionMetric.indexingLookback]: EVENT_END, +}; + +/** + * Returns ECS field in which metric value is stored + * @deprecated getMetricField is kept here only as a reference. It will be superseded with EventLog implementation + * + * @param metric - execution metric + * @returns ECS field + */ +export const getMetricField = (metric: T) => METRIC_FIELDS[metric]; + +/** + * @deprecated sortByTimeDesc is kept here only as a reference. It will be superseded with EventLog implementation + */ +export const sortByTimeDesc: SearchSort = [{ [TIMESTAMP]: 'desc' }, { [EVENT_SEQUENCE]: 'desc' }]; + +/** + * Builds aggregation to retrieve the most recent metric value + * @deprecated getMetricAggregation is kept here only as a reference. It will be superseded with EventLog implementation + * + * @param metric - execution metric + * @returns aggregation + */ +export const getMetricAggregation = (metric: ExecutionMetric) => ({ + filter: { + term: { [EVENT_ACTION]: metric }, + }, + aggs: { + event: { + top_hits: { + size: 1, + sort: sortByTimeDesc, + _source: [TIMESTAMP, getMetricField(metric)], + }, + }, + }, +}); + +/** + * Builds aggregation to retrieve the most recent log entry with the given status + * @deprecated getLastEntryAggregation is kept here only as a reference. It will be superseded with EventLog implementation + * + * @param status - rule execution status + * @returns aggregation + */ +export const getLastEntryAggregation = (status: RuleExecutionStatus) => ({ + filter: { + term: { [RULE_STATUS]: status }, + }, + aggs: { + event: { + top_hits: { + sort: sortByTimeDesc, + size: 1, + }, + }, + }, +}); 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 new file mode 100644 index 0000000000000..ca589fd1d584f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts @@ -0,0 +1,69 @@ +/* + * 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 { SavedObjectsFindResult } from '../../../../../../../src/core/server'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; +import { IRuleStatusSOAttributes } from '../rules/types'; + +export enum ExecutionMetric { + 'executionGap' = 'executionGap', + 'searchDurationMax' = 'searchDurationMax', + 'indexingDurationMax' = 'indexingDurationMax', + 'indexingLookback' = 'indexingLookback', +} + +export type ExecutionMetricValue = { + [ExecutionMetric.executionGap]: number; + [ExecutionMetric.searchDurationMax]: number; + [ExecutionMetric.indexingDurationMax]: number; + [ExecutionMetric.indexingLookback]: Date; +}[T]; + +export interface FindExecutionLogArgs { + ruleId: string; + spaceId: string; + logsCount?: number; +} + +export interface FindBulkExecutionLogArgs { + ruleIds: string[]; + spaceId: string; + logsCount?: number; +} + +export interface LogStatusChangeArgs { + ruleId: string; + spaceId: string; + newStatus: RuleExecutionStatus; + namespace?: string; + message?: string; +} + +export interface ExecutionMetricArgs { + ruleId: string; + spaceId: string; + namespace?: string; + metric: T; + value: ExecutionMetricValue; +} + +export interface FindBulkExecutionLogResponse { + [ruleId: string]: IRuleStatusSOAttributes[] | undefined; +} + +export interface IRuleExecutionLogClient { + find: ( + args: FindExecutionLogArgs + ) => Promise>>; + findBulk: (args: FindBulkExecutionLogArgs) => Promise; + create: (event: IRuleStatusSOAttributes, spaceId: string) => Promise; + update: (id: string, event: IRuleStatusSOAttributes, spaceId: string) => 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 new file mode 100644 index 0000000000000..7cd0b2b345438 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/with_rule_execution_log.ts @@ -0,0 +1,75 @@ +/* + * 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 } from '../../../../../alerting/common'; +import { AlertTypeWithExecutor, RuleDataPluginService } from '../../../../../rule_registry/server'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; +import { RuleExecutionLogClient } from './rule_execution_log_client'; +import { IRuleExecutionLogClient } from './types'; + +export interface ExecutionLogServices { + ruleExecutionLogClient: IRuleExecutionLogClient; + logger: Logger; +} + +type WithRuleExecutionLog = (args: { + logger: Logger; + ruleDataService: RuleDataPluginService; +}) => < + 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/rules/delete_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts index cd7bbfd9fced7..86a60da7808ef 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 @@ -7,25 +7,25 @@ import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; import { rulesClientMock } from '../../../../../alerting/server/mocks'; -import { ruleStatusSavedObjectsClientMock } from '../signals/__mocks__/rule_status_saved_objects_client.mock'; import { deleteRules } from './delete_rules'; 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'; 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 = ruleStatusSavedObjectsClientMock.create(); + ruleStatusClient = new RuleExecutionLogClient(); }); it('should delete the rule along with its notifications, actions, and statuses', async () => { @@ -54,12 +54,7 @@ describe('deleteRules', () => { savedObjectsClient, ruleStatusClient, id: 'ruleId', - ruleStatuses: { - total: 0, - per_page: 0, - page: 0, - saved_objects: [ruleStatus], - }, + ruleStatuses: [ruleStatus], }; await deleteRules(rule); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts index e1385eb05a6b4..2c68887c73f0d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts @@ -19,5 +19,5 @@ export const deleteRules = async ({ await rulesClient.delete({ id }); await deleteNotifications({ rulesClient, ruleAlertId: id }); await deleteRuleActionsSavedObject({ ruleAlertId: id, savedObjectsClient }); - ruleStatuses.saved_objects.forEach(async (obj) => ruleStatusClient.delete(obj.id)); + ruleStatuses.forEach(async (obj) => ruleStatusClient.delete(obj.id)); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts index 1a81cf0cb5ebe..b727596cd3b02 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts @@ -5,16 +5,17 @@ * 2.0. */ -import { SavedObjectsClientContract } from 'kibana/server'; import { SanitizedAlert } from '../../../../../alerting/common'; import { RulesClient } from '../../../../../alerting/server'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; +import { IRuleExecutionLogClient } from '../rule_execution_log/types'; import { RuleParams } from '../schemas/rule_schemas'; -import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; interface EnableRuleArgs { rule: SanitizedAlert; rulesClient: RulesClient; - savedObjectsClient: SavedObjectsClientContract; + ruleStatusClient: IRuleExecutionLogClient; + spaceId: string; } /** @@ -22,26 +23,32 @@ interface EnableRuleArgs { * * @param rule - rule to enable * @param rulesClient - Alerts client - * @param savedObjectsClient - Saved Objects client + * @param ruleStatusClient - ExecLog client */ -export const enableRule = async ({ rule, rulesClient, savedObjectsClient }: EnableRuleArgs) => { +export const enableRule = async ({ + rule, + rulesClient, + ruleStatusClient, + spaceId, +}: EnableRuleArgs) => { await rulesClient.enable({ id: rule.id }); - const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const ruleCurrentStatus = await ruleStatusClient.find({ - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: rule.id, - searchFields: ['alertId'], + logsCount: 1, + ruleId: rule.id, + spaceId, }); // set current status for this rule to be 'going to run' - if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { - const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; - await ruleStatusClient.update(currentStatusToDisable.id, { - ...currentStatusToDisable.attributes, - status: 'going to run', - }); + if (ruleCurrentStatus && ruleCurrentStatus.length > 0) { + const currentStatusToDisable = ruleCurrentStatus[0]; + await ruleStatusClient.update( + currentStatusToDisable.id, + { + ...currentStatusToDisable.attributes, + status: RuleExecutionStatus['going to run'], + }, + spaceId + ); } }; 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 826b197cfe094..98b39e3a5ff27 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 @@ -7,15 +7,16 @@ import { PatchRulesOptions } from './types'; import { rulesClientMock } from '../../../../../alerting/server/mocks'; -import { savedObjectsClientMock } from '../../../../../../../src/core/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'; export const getPatchRulesOptionsMock = (): PatchRulesOptions => ({ author: ['Elastic'], buildingBlockType: undefined, rulesClient: rulesClientMock.create(), - savedObjectsClient: savedObjectsClientMock.create(), + spaceId: 'default', + ruleStatusClient: new RuleExecutionLogClient(), anomalyThreshold: undefined, description: 'some description', enabled: true, @@ -66,7 +67,8 @@ export const getPatchMlRulesOptionsMock = (): PatchRulesOptions => ({ author: ['Elastic'], buildingBlockType: undefined, rulesClient: rulesClientMock.create(), - savedObjectsClient: savedObjectsClientMock.create(), + spaceId: 'default', + ruleStatusClient: new RuleExecutionLogClient(), anomalyThreshold: 55, description: 'some description', enabled: true, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts index 60e406255494a..39de70f702bd8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts @@ -31,7 +31,8 @@ export const patchRules = async ({ rulesClient, author, buildingBlockType, - savedObjectsClient, + ruleStatusClient, + spaceId, description, eventCategoryOverride, falsePositives, @@ -200,7 +201,7 @@ export const patchRules = async ({ if (rule.enabled && enabled === false) { await rulesClient.disable({ id: rule.id }); } else if (!rule.enabled && enabled === true) { - await enableRule({ rule, rulesClient, savedObjectsClient }); + await enableRule({ rule, rulesClient, ruleStatusClient, spaceId }); } else { // enabled is null or undefined and we do not touch the rule } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 7274614e2c9ba..31e1ba5201020 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -13,6 +13,7 @@ import { SavedObjectAttributes, SavedObjectsFindResponse, SavedObjectsClientContract, + SavedObjectsFindResult, } from 'kibana/server'; import type { MachineLearningJobIdOrUndefined, @@ -86,7 +87,7 @@ import { QueryFilterOrUndefined, FieldsOrUndefined, SortOrderOrUndefined, - JobStatus, + RuleExecutionStatus, LastSuccessAt, StatusDate, LastSuccessMessage, @@ -106,7 +107,7 @@ import { Alert, SanitizedAlert } from '../../../../../alerting/common'; import { SIGNALS_ID } from '../../../../common/constants'; import { PartialFilter } from '../types'; import { RuleParams } from '../schemas/rule_schemas'; -import { RuleStatusSavedObjectsClient } from '../signals/rule_status_saved_objects_client'; +import { IRuleExecutionLogClient } from '../rule_execution_log/types'; export type RuleAlertType = Alert; @@ -118,7 +119,7 @@ export interface IRuleStatusSOAttributes extends Record { lastFailureMessage: LastFailureMessage | null | undefined; lastSuccessAt: LastSuccessAt | null | undefined; lastSuccessMessage: LastSuccessMessage | null | undefined; - status: JobStatus | null | undefined; + status: RuleExecutionStatus | null | undefined; lastLookBackDate: string | null | undefined; gap: string | null | undefined; bulkCreateTimeDurations: string[] | null | undefined; @@ -132,7 +133,7 @@ export interface IRuleStatusResponseAttributes { last_failure_message: LastFailureMessage | null | undefined; last_success_at: LastSuccessAt | null | undefined; last_success_message: LastSuccessMessage | null | undefined; - status: JobStatus | null | undefined; + status: RuleExecutionStatus | null | undefined; last_look_back_date: string | null | undefined; // NOTE: This is no longer used on the UI, but left here in case users are using it within the API gap: string | null | undefined; bulk_create_time_durations: string[] | null | undefined; @@ -266,14 +267,16 @@ export interface CreateRulesOptions { } export interface UpdateRulesOptions { - savedObjectsClient: SavedObjectsClientContract; + spaceId: string; + ruleStatusClient: IRuleExecutionLogClient; rulesClient: RulesClient; defaultOutputIndex: string; ruleUpdate: UpdateRulesSchema; } export interface PatchRulesOptions { - savedObjectsClient: SavedObjectsClientContract; + spaceId: string; + ruleStatusClient: IRuleExecutionLogClient; rulesClient: RulesClient; anomalyThreshold: AnomalyThresholdOrUndefined; author: AuthorOrUndefined; @@ -332,8 +335,8 @@ export interface ReadRuleOptions { export interface DeleteRuleOptions { rulesClient: RulesClient; savedObjectsClient: SavedObjectsClientContract; - ruleStatusClient: RuleStatusSavedObjectsClient; - ruleStatuses: SavedObjectsFindResponse; + ruleStatusClient: IRuleExecutionLogClient; + ruleStatuses: Array>; id: Id; } 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 b88c7f0450cac..556a95d816131 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 @@ -5,21 +5,21 @@ * 2.0. */ -import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; 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'; jest.mock('./patch_rules'); describe('updatePrepackagedRules', () => { let rulesClient: ReturnType; - let savedObjectsClient: ReturnType; + let ruleStatusClient: ReturnType; beforeEach(() => { rulesClient = rulesClientMock.create(); - savedObjectsClient = savedObjectsClientMock.create(); + ruleStatusClient = new RuleExecutionLogClient(); }); it('should omit actions and enabled when calling patchRules', async () => { @@ -37,7 +37,8 @@ describe('updatePrepackagedRules', () => { await updatePrepackagedRules( rulesClient, - savedObjectsClient, + 'default', + ruleStatusClient, [{ ...prepackagedRule, actions }], outputIndex ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts index 872a3b69d27ed..d60cf1ef016df 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { SavedObjectsClientContract } from 'kibana/server'; import { chunk } from 'lodash/fp'; import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; import { RulesClient, PartialAlert } from '../../../../../alerting/server'; @@ -13,6 +12,7 @@ import { patchRules } from './patch_rules'; import { readRules } from './read_rules'; import { PartialFilter } from '../types'; import { RuleParams } from '../schemas/rule_schemas'; +import { IRuleExecutionLogClient } from '../rule_execution_log/types'; /** * How many rules to update at a time is set to 50 from errors coming from @@ -44,19 +44,27 @@ export const UPDATE_CHUNK_SIZE = 50; * This implements a chunked approach to not saturate network connections and * avoid being a "noisy neighbor". * @param rulesClient Alerting client - * @param savedObjectsClient Saved object client + * @param spaceId Current user spaceId + * @param ruleStatusClient Rule execution log client * @param rules The rules to apply the update for * @param outputIndex The output index to apply the update to. */ export const updatePrepackagedRules = async ( rulesClient: RulesClient, - savedObjectsClient: SavedObjectsClientContract, + spaceId: string, + ruleStatusClient: IRuleExecutionLogClient, rules: AddPrepackagedRulesSchemaDecoded[], outputIndex: string ): Promise => { const ruleChunks = chunk(UPDATE_CHUNK_SIZE, rules); for (const ruleChunk of ruleChunks) { - const rulePromises = createPromises(rulesClient, savedObjectsClient, ruleChunk, outputIndex); + const rulePromises = createPromises( + rulesClient, + spaceId, + ruleStatusClient, + ruleChunk, + outputIndex + ); await Promise.all(rulePromises); } }; @@ -64,14 +72,16 @@ export const updatePrepackagedRules = async ( /** * Creates promises of the rules and returns them. * @param rulesClient Alerting client - * @param savedObjectsClient Saved object client + * @param spaceId Current user spaceId + * @param ruleStatusClient Rule execution log client * @param rules The rules to apply the update for * @param outputIndex The output index to apply the update to. * @returns Promise of what was updated. */ export const createPromises = ( rulesClient: RulesClient, - savedObjectsClient: SavedObjectsClientContract, + spaceId: string, + ruleStatusClient: IRuleExecutionLogClient, rules: AddPrepackagedRulesSchemaDecoded[], outputIndex: string ): Array | null>> => { @@ -143,7 +153,8 @@ export const createPromises = ( outputIndex, rule: existingRule, savedId, - savedObjectsClient, + spaceId, + ruleStatusClient, meta, filters, index, 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 778337a3650c7..c72b225c2fee2 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 @@ -5,24 +5,26 @@ * 2.0. */ -import { UpdateRulesOptions } from './types'; import { rulesClientMock } from '../../../../../alerting/server/mocks'; -import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; import { - getUpdateRulesSchemaMock, getUpdateMachineLearningSchemaMock, + getUpdateRulesSchemaMock, } from '../../../../common/detection_engine/schemas/request/rule_schemas.mock'; +import { RuleExecutionLogClient } from '../rule_execution_log/__mocks__/rule_execution_log_client'; +import { UpdateRulesOptions } from './types'; export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({ + spaceId: 'default', rulesClient: rulesClientMock.create(), - savedObjectsClient: savedObjectsClientMock.create(), + ruleStatusClient: new RuleExecutionLogClient(), defaultOutputIndex: '.siem-signals-default', ruleUpdate: getUpdateRulesSchemaMock(), }); export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({ + spaceId: 'default', rulesClient: rulesClientMock.create(), - savedObjectsClient: savedObjectsClientMock.create(), + ruleStatusClient: new RuleExecutionLogClient(), defaultOutputIndex: '.siem-signals-default', ruleUpdate: getUpdateMachineLearningSchemaMock(), }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts index e0be646dc3f39..7ef2e800c23a4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts @@ -18,8 +18,9 @@ import { InternalRuleUpdate, RuleParams } from '../schemas/rule_schemas'; import { enableRule } from './enable_rule'; export const updateRules = async ({ + spaceId, rulesClient, - savedObjectsClient, + ruleStatusClient, defaultOutputIndex, ruleUpdate, }: UpdateRulesOptions): Promise | null> => { @@ -88,7 +89,7 @@ export const updateRules = async ({ if (existingRule.enabled && enabled === false) { await rulesClient.disable({ id: existingRule.id }); } else if (!existingRule.enabled && enabled === true) { - await enableRule({ rule: existingRule, rulesClient, savedObjectsClient }); + await enableRule({ rule: existingRule, rulesClient, ruleStatusClient, spaceId }); } return { ...update, enabled }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts index a215da021d15a..577d52c789857 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts @@ -31,6 +31,7 @@ import { transformRuleToAlertAction } from '../../../../common/detection_engine/ import { SanitizedAlert } from '../../../../../alerting/common'; import { IRuleStatusSOAttributes } from '../rules/types'; import { transformTags } from '../routes/rules/utils'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; // These functions provide conversions from the request API schema to the internal rule schema and from the internal rule schema // to the response API schema. This provides static type-check assurances that the internal schema is in sync with the API schema for @@ -315,7 +316,7 @@ export const mergeAlertWithSidecarStatus = ( lastFailureMessage: `Reason: ${alert.executionStatus.error?.reason} Message: ${alert.executionStatus.error?.message}`, lastFailureAt: alert.executionStatus.lastExecutionDate.toISOString(), statusDate: alert.executionStatus.lastExecutionDate.toISOString(), - status: 'failed', + status: RuleExecutionStatus.failed, }; } return status; 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 0fed141ca4dbc..5f4a9f5f7a422 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, SavedObjectsFindResponse } from '../../../../../../../../src/core/server'; +import { SavedObject, SavedObjectsFindResult } from '../../../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks'; import { IRuleStatusSOAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; @@ -23,6 +23,7 @@ import { getListArrayMock } from '../../../../../common/detection_engine/schemas import { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; import { RuleParams } from '../../schemas/rule_schemas'; import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; export const sampleRuleSO = (params: T): SavedObject> => { return { @@ -326,7 +327,7 @@ export const sampleSignalHit = (): SignalHit => ({ type: 'query', threat: [], version: 1, - status: 'succeeded', + status: RuleExecutionStatus.succeeded, status_date: '2020-02-22T16:47:50.047Z', last_success_at: '2020-02-22T16:47:50.047Z', last_success_message: 'succeeded', @@ -391,7 +392,7 @@ export const sampleThresholdSignalHit = (): SignalHit => ({ type: 'query', threat: [], version: 1, - status: 'succeeded', + status: RuleExecutionStatus.succeeded, status_date: '2020-02-22T16:47:50.047Z', last_success_at: '2020-02-22T16:47:50.047Z', last_success_message: 'succeeded', @@ -712,7 +713,7 @@ export const exampleRuleStatus: () => SavedObject = () attributes: { alertId: 'f4b8e31d-cf93-4bde-a265-298bde885cd7', statusDate: '2020-03-27T22:55:59.517Z', - status: 'succeeded', + status: RuleExecutionStatus.succeeded, lastFailureAt: null, lastSuccessAt: '2020-03-27T22:55:59.517Z', lastFailureMessage: null, @@ -729,14 +730,9 @@ export const exampleRuleStatus: () => SavedObject = () export const exampleFindRuleStatusResponse: ( mockStatuses: Array> -) => SavedObjectsFindResponse = ( +) => Array> = ( mockStatuses = [exampleRuleStatus()] -) => ({ - total: 1, - per_page: 6, - page: 1, - saved_objects: mockStatuses.map((obj) => ({ ...obj, score: 1 })), -}); +) => mockStatuses.map((obj) => ({ ...obj, score: 1 })); export const mockLogger = loggingSystemMock.createLogger(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts index 3a30da170d3f2..8c0790761a5e0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts @@ -20,6 +20,7 @@ import { } from '../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { SIGNALS_TEMPLATE_VERSION } from '../routes/index/get_signals_template'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; describe('buildSignal', () => { beforeEach(() => { @@ -84,7 +85,7 @@ describe('buildSignal', () => { type: 'query', threat: [], version: 1, - status: 'succeeded', + status: RuleExecutionStatus.succeeded, status_date: '2020-02-22T16:47:50.047Z', last_success_at: '2020-02-22T16:47:50.047Z', last_success_message: 'succeeded', @@ -171,7 +172,7 @@ describe('buildSignal', () => { type: 'query', threat: [], version: 1, - status: 'succeeded', + status: RuleExecutionStatus.succeeded, status_date: '2020-02-22T16:47:50.047Z', last_success_at: '2020-02-22T16:47:50.047Z', last_success_message: 'succeeded', 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 index 08d969a544de1..8c4ffdb2a6c4a 100644 --- 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 @@ -10,6 +10,7 @@ import { SavedObject } from 'src/core/server'; import { IRuleStatusSOAttributes } from '../rules/types'; import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; import { getRuleStatusSavedObjects } from './get_rule_status_saved_objects'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; interface RuleStatusParams { alertId: string; @@ -24,7 +25,7 @@ export const createNewRuleStatus = async ({ return ruleStatusClient.create({ alertId, statusDate: now, - status: 'going to run', + status: RuleExecutionStatus['going to run'], lastFailureAt: null, lastSuccessAt: null, lastFailureMessage: null, @@ -44,8 +45,8 @@ export const getOrCreateRuleStatuses = async ({ alertId, ruleStatusClient, }); - if (ruleStatuses.saved_objects.length > 0) { - return ruleStatuses.saved_objects; + if (ruleStatuses.length > 0) { + return ruleStatuses; } const newStatus = await createNewRuleStatus({ alertId, ruleStatusClient }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts index 4f3acf19cf11f..dd3a2826018e2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObjectsFindResponse } from 'kibana/server'; +import { SavedObjectsFindResult } from 'kibana/server'; import { IRuleStatusSOAttributes } from '../rules/types'; import { MAX_RULE_STATUSES } from './rule_status_service'; import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; @@ -18,7 +18,7 @@ interface GetRuleStatusSavedObject { export const getRuleStatusSavedObjects = async ({ alertId, ruleStatusClient, -}: GetRuleStatusSavedObject): Promise> => { +}: GetRuleStatusSavedObject): Promise>> => { return ruleStatusClient.find({ perPage: MAX_RULE_STATUSES, sortField: 'statusDate', 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/signals/rule_status_saved_objects_client.ts index 25d315279ad60..62f02ad6a251a 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/signals/rule_status_saved_objects_client.ts @@ -11,7 +11,7 @@ import { SavedObject, SavedObjectsUpdateResponse, SavedObjectsFindOptions, - SavedObjectsFindResponse, + SavedObjectsFindResult, } from '../../../../../../../src/core/server'; import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; import { IRuleStatusSOAttributes } from '../rules/types'; @@ -20,7 +20,7 @@ import { buildChunkedOrFilter } from './utils'; export interface RuleStatusSavedObjectsClient { find: ( options?: Omit - ) => Promise>; + ) => Promise>>; findBulk: (ids: string[], statusesPerId: number) => Promise; create: (attributes: IRuleStatusSOAttributes) => Promise>; update: ( @@ -30,18 +30,20 @@ export interface RuleStatusSavedObjectsClient { delete: (id: string) => Promise<{}>; } -interface FindBulkResponse { +export interface FindBulkResponse { [key: string]: IRuleStatusSOAttributes[] | undefined; } export const ruleStatusSavedObjectsClientFactory = ( savedObjectsClient: SavedObjectsClientContract ): RuleStatusSavedObjectsClient => ({ - find: (options) => - savedObjectsClient.find({ + find: async (options) => { + const result = await savedObjectsClient.find({ ...options, type: ruleStatusSavedObjectType, - }), + }); + return result.saved_objects; + }, findBulk: async (ids, statusesPerId) => { if (ids.length === 0) { return {}; 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 index 7f2962ae0a6c8..ec843351d74b5 100644 --- 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 @@ -13,6 +13,7 @@ import { MAX_RULE_STATUSES, } from './rule_status_service'; import { exampleRuleStatus, exampleFindRuleStatusResponse } from './__mocks__/es_results'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; const expectIsoDateString = expect.stringMatching(/2.*Z$/); const buildStatuses = (n: number) => @@ -25,9 +26,11 @@ const buildStatuses = (n: number) => describe('buildRuleStatusAttributes', () => { it('generates a new date on each call', async () => { - const { statusDate } = buildRuleStatusAttributes('going to run'); + const { statusDate } = buildRuleStatusAttributes(RuleExecutionStatus['going to run']); await new Promise((resolve) => setTimeout(resolve, 10)); // ensure time has passed - const { statusDate: statusDate2 } = buildRuleStatusAttributes('going to run'); + const { statusDate: statusDate2 } = buildRuleStatusAttributes( + RuleExecutionStatus['going to run'] + ); expect(statusDate).toEqual(expectIsoDateString); expect(statusDate2).toEqual(expectIsoDateString); @@ -35,7 +38,7 @@ describe('buildRuleStatusAttributes', () => { }); it('returns a status and statusDate if "going to run"', () => { - const result = buildRuleStatusAttributes('going to run'); + const result = buildRuleStatusAttributes(RuleExecutionStatus['going to run']); expect(result).toEqual({ status: 'going to run', statusDate: expectIsoDateString, @@ -43,7 +46,7 @@ describe('buildRuleStatusAttributes', () => { }); it('returns success fields if "success"', () => { - const result = buildRuleStatusAttributes('succeeded', 'success message'); + const result = buildRuleStatusAttributes(RuleExecutionStatus.succeeded, 'success message'); expect(result).toEqual({ status: 'succeeded', statusDate: expectIsoDateString, @@ -56,7 +59,7 @@ describe('buildRuleStatusAttributes', () => { it('returns warning fields if "warning"', () => { const result = buildRuleStatusAttributes( - 'warning', + RuleExecutionStatus.warning, 'some indices missing timestamp override field' ); expect(result).toEqual({ @@ -70,7 +73,7 @@ describe('buildRuleStatusAttributes', () => { }); it('returns failure fields if "failed"', () => { - const result = buildRuleStatusAttributes('failed', 'failure message'); + const result = buildRuleStatusAttributes(RuleExecutionStatus.failed, 'failure message'); expect(result).toEqual({ status: 'failed', statusDate: expectIsoDateString, 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 index dc4663db6c74d..0d51a6663b709 100644 --- 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 @@ -6,7 +6,7 @@ */ import { assertUnreachable } from '../../../../common/utility_types'; -import { JobStatus } from '../../../../common/detection_engine/schemas/common/schemas'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; import { IRuleStatusSOAttributes } from '../rules/types'; import { getOrCreateRuleStatuses } from './get_or_create_rule_statuses'; import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; @@ -29,7 +29,7 @@ export interface RuleStatusService { } export const buildRuleStatusAttributes: ( - status: JobStatus, + status: RuleExecutionStatus, message?: string, attributes?: Attributes ) => Partial = (status, message, attributes = {}) => { @@ -41,35 +41,35 @@ export const buildRuleStatusAttributes: ( }; switch (status) { - case 'succeeded': { + case RuleExecutionStatus.succeeded: { return { ...baseAttributes, lastSuccessAt: now, lastSuccessMessage: message, }; } - case 'warning': { + case RuleExecutionStatus.warning: { return { ...baseAttributes, lastSuccessAt: now, lastSuccessMessage: message, }; } - case 'partial failure': { + case RuleExecutionStatus['partial failure']: { return { ...baseAttributes, lastSuccessAt: now, lastSuccessMessage: message, }; } - case 'failed': { + case RuleExecutionStatus.failed: { return { ...baseAttributes, lastFailureAt: now, lastFailureMessage: message, }; } - case 'going to run': { + case RuleExecutionStatus['going to run']: { return baseAttributes; } } @@ -93,7 +93,7 @@ export const ruleStatusServiceFactory = async ({ await ruleStatusClient.update(currentStatus.id, { ...currentStatus.attributes, - ...buildRuleStatusAttributes('going to run'), + ...buildRuleStatusAttributes(RuleExecutionStatus['going to run']), }); }, @@ -105,7 +105,7 @@ export const ruleStatusServiceFactory = async ({ await ruleStatusClient.update(currentStatus.id, { ...currentStatus.attributes, - ...buildRuleStatusAttributes('succeeded', message, attributes), + ...buildRuleStatusAttributes(RuleExecutionStatus.succeeded, message, attributes), }); }, @@ -117,7 +117,7 @@ export const ruleStatusServiceFactory = async ({ await ruleStatusClient.update(currentStatus.id, { ...currentStatus.attributes, - ...buildRuleStatusAttributes('partial failure', message, attributes), + ...buildRuleStatusAttributes(RuleExecutionStatus['partial failure'], message, attributes), }); }, @@ -130,7 +130,7 @@ export const ruleStatusServiceFactory = async ({ const failureAttributes = { ...currentStatus.attributes, - ...buildRuleStatusAttributes('failed', message, 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 diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index a68280379fad3..dd6081b6c9127 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -69,6 +69,7 @@ import { REFERENCE_RULE_ALERT_TYPE_ID, REFERENCE_RULE_PERSISTENCE_ALERT_TYPE_ID, CUSTOM_ALERT_TYPE_ID, + DEFAULT_SPACE_ID, } from '../common/constants'; import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerLimitedConcurrencyRoutes } from './endpoint/routes/limited_concurrency'; @@ -91,6 +92,7 @@ import { licenseService } from './lib/license'; import { PolicyWatcher } from './endpoint/lib/policy/license_watch'; import { parseExperimentalConfigValue } from '../common/experimental_features'; import { migrateArtifactsToFleet } from './endpoint/lib/artifacts/migrate_artifacts_to_fleet'; +import { RuleExecutionLogClient } from './lib/detection_engine/rule_execution_log/rule_execution_log_client'; import { getKibanaPrivilegesFeaturePrivileges } from './features'; import { EndpointMetadataService } from './endpoint/services/metadata'; @@ -184,6 +186,13 @@ export class Plugin implements IPlugin ({ getAppClient: () => this.appClientFactory.create(request), + getSpaceId: () => plugins.spaces?.spacesService?.getSpaceId(request) || DEFAULT_SPACE_ID, + getExecutionLogClient: () => + new RuleExecutionLogClient({ + ruleDataService: plugins.ruleRegistry.ruleDataService, + // TODO check if savedObjects.client contains spaceId + savedObjectsClient: context.core.savedObjects.client, + }), }) ); @@ -202,11 +211,10 @@ export class Plugin implements IPlugin { - const componentTemplateName = ruleDataService.getFullAssetName('security.alerts-mappings'); - if (!ruleDataService.isWriteEnabled()) { return; } + const componentTemplateName = ruleDataService.getFullAssetName('security.alerts-mappings'); await ruleDataService.createOrUpdateComponentTemplate({ name: componentTemplateName, @@ -245,8 +253,6 @@ export class Plugin implements IPlugin initializeRuleDataTemplatesPromise ); - // sec - // Register reference rule types via rule-registry this.setupPlugins.alerting.registerType(createQueryAlertType(ruleDataClient, this.logger)); this.setupPlugins.alerting.registerType(createEqlAlertType(ruleDataClient, this.logger)); diff --git a/x-pack/plugins/security_solution/server/types.ts b/x-pack/plugins/security_solution/server/types.ts index ea63e188ab26d..f47944b5fd392 100644 --- a/x-pack/plugins/security_solution/server/types.ts +++ b/x-pack/plugins/security_solution/server/types.ts @@ -11,11 +11,14 @@ import type { LicensingApiRequestHandlerContext } from '../../licensing/server'; import type { AlertingApiRequestHandlerContext } from '../../alerting/server'; import { AppClient } from './client'; +import { RuleExecutionLogClient } from './lib/detection_engine/rule_execution_log/rule_execution_log_client'; export { AppClient }; export interface AppRequestContext { getAppClient: () => AppClient; + getSpaceId: () => string; + getExecutionLogClient: () => RuleExecutionLogClient; } export type SecuritySolutionRequestHandlerContext = RequestHandlerContext & {