diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts index 910e1ecaa508f..518e4aa903d95 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts @@ -11,8 +11,13 @@ export const ruleExecutionLogClientMock = { create: (): jest.Mocked => ({ find: jest.fn(), findBulk: jest.fn(), - update: jest.fn(), - delete: jest.fn(), + + getLastFailures: jest.fn(), + getCurrentStatus: jest.fn(), + getCurrentStatusBulk: jest.fn(), + + deleteCurrentStatus: jest.fn(), + logStatusChange: jest.fn(), logExecutionMetrics: jest.fn(), }), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts index a3fb50f1f6b0b..1cef186e70837 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts @@ -7,18 +7,25 @@ import { sum } from 'lodash'; import { SavedObjectsClientContract } from '../../../../../../../../src/core/server'; -import { IEventLogService } from '../../../../../../event_log/server'; +import { IEventLogClient, IEventLogService } from '../../../../../../event_log/server'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { IRuleStatusSOAttributes } from '../../rules/types'; import { SavedObjectsAdapter } from '../saved_objects_adapter/saved_objects_adapter'; import { FindBulkExecutionLogArgs, FindExecutionLogArgs, + GetCurrentStatusArgs, + GetCurrentStatusBulkArgs, + GetCurrentStatusBulkResult, + GetLastFailuresArgs, IRuleExecutionLogClient, LogExecutionMetricsArgs, LogStatusChangeArgs, - UpdateExecutionLogArgs, } from '../types'; import { EventLogClient } from './event_log_client'; +const MAX_LAST_FAILURES = 5; + export class EventLogAdapter implements IRuleExecutionLogClient { private eventLogClient: EventLogClient; /** @@ -28,38 +35,44 @@ export class EventLogAdapter implements IRuleExecutionLogClient { */ private savedObjectsAdapter: IRuleExecutionLogClient; - constructor(eventLogService: IEventLogService, savedObjectsClient: SavedObjectsClientContract) { - this.eventLogClient = new EventLogClient(eventLogService); + constructor( + eventLogService: IEventLogService, + eventLogClient: IEventLogClient, + savedObjectsClient: SavedObjectsClientContract + ) { + this.eventLogClient = new EventLogClient(eventLogService, eventLogClient); this.savedObjectsAdapter = new SavedObjectsAdapter(savedObjectsClient); } + /** @deprecated */ public async find(args: FindExecutionLogArgs) { return this.savedObjectsAdapter.find(args); } + /** @deprecated */ public async findBulk(args: FindBulkExecutionLogArgs) { return this.savedObjectsAdapter.findBulk(args); } - public async update(args: UpdateExecutionLogArgs) { - const { attributes, spaceId, ruleId, ruleName, ruleType } = args; + public getLastFailures(args: GetLastFailuresArgs): Promise { + const { ruleId } = args; + return this.eventLogClient.getLastStatusChanges({ + ruleId, + count: MAX_LAST_FAILURES, + includeStatuses: [RuleExecutionStatus.failed], + }); + } - await this.savedObjectsAdapter.update(args); + public getCurrentStatus(args: GetCurrentStatusArgs): Promise { + return this.savedObjectsAdapter.getCurrentStatus(args); + } - // EventLog execution events are immutable, so we just log a status change istead of updating previous - if (attributes.status) { - this.eventLogClient.logStatusChange({ - ruleName, - ruleType, - ruleId, - newStatus: attributes.status, - spaceId, - }); - } + public getCurrentStatusBulk(args: GetCurrentStatusBulkArgs): Promise { + return this.savedObjectsAdapter.getCurrentStatusBulk(args); } - public async delete(id: string) { - await this.savedObjectsAdapter.delete(id); + public async deleteCurrentStatus(ruleId: string): Promise { + await this.savedObjectsAdapter.deleteCurrentStatus(ruleId); // EventLog execution events are immutable, nothing to do here } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_client.ts index d85c67e422035..42c7915b25a80 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_client.ts @@ -7,11 +7,13 @@ import { SavedObjectsUtils } from '../../../../../../../../src/core/server'; import { + IEventLogClient, IEventLogger, IEventLogService, SAVED_OBJECT_REL_PRIMARY, } from '../../../../../../event_log/server'; import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { IRuleStatusSOAttributes } from '../../rules/types'; import { LogStatusChangeArgs } from '../types'; import { RuleExecutionLogAction, @@ -29,13 +31,6 @@ const statusSeverityDict: Record = { [RuleExecutionStatus.failed]: 30, }; -interface FindExecutionLogArgs { - ruleIds: string[]; - spaceId: string; - logsCount?: number; - statuses?: RuleExecutionStatus[]; -} - interface LogExecutionMetricsArgs { ruleId: string; ruleName: string; @@ -50,24 +45,80 @@ interface EventLogExecutionMetrics { executionGapDuration?: number; } +interface GetLastStatusChangesArgs { + ruleId: string; + count: number; + includeStatuses?: RuleExecutionStatus[]; +} + interface IExecLogEventLogClient { - find: (args: FindExecutionLogArgs) => Promise<{}>; + getLastStatusChanges(args: GetLastStatusChangesArgs): Promise; logStatusChange: (args: LogStatusChangeArgs) => void; logExecutionMetrics: (args: LogExecutionMetricsArgs) => void; } export class EventLogClient implements IExecLogEventLogClient { + private readonly eventLogClient: IEventLogClient; + private readonly eventLogger: IEventLogger; private sequence = 0; - private eventLogger: IEventLogger; - constructor(eventLogService: IEventLogService) { + constructor(eventLogService: IEventLogService, eventLogClient: IEventLogClient) { + this.eventLogClient = eventLogClient; this.eventLogger = eventLogService.getLogger({ event: { provider: RULE_EXECUTION_LOG_PROVIDER }, }); } - public async find({ ruleIds, spaceId, statuses, logsCount = 1 }: FindExecutionLogArgs) { - return {}; // TODO implement + public async getLastStatusChanges( + args: GetLastStatusChangesArgs + ): Promise { + const soType = ALERT_SAVED_OBJECT_TYPE; + const soIds = [args.ruleId]; + const count = args.count; + const includeStatuses = (args.includeStatuses ?? []).map((status) => `"${status}"`); + + const filterBy: string[] = [ + `event.provider: ${RULE_EXECUTION_LOG_PROVIDER}`, + 'event.kind: event', + `event.action: ${RuleExecutionLogAction['status-change']}`, + includeStatuses.length > 0 + ? `kibana.alert.rule.execution.status:${includeStatuses.join(' ')}` + : '', + ]; + + const kqlFilter = filterBy + .filter(Boolean) + .map((item) => `(${item})`) + .join(' and '); + + const findResult = await this.eventLogClient.findEventsBySavedObjectIds(soType, soIds, { + page: 1, + per_page: count, + sort_field: '@timestamp', + sort_order: 'desc', + filter: kqlFilter, + }); + + return findResult.data.map((event) => { + const statusDate = event?.['@timestamp'] ?? new Date().toISOString(); + const status = event?.kibana?.alert?.rule?.execution?.status as + | RuleExecutionStatus + | undefined; + const message = event?.message ?? ''; + + return { + statusDate, + status, + lastFailureAt: status === RuleExecutionStatus.failed ? statusDate : undefined, + lastFailureMessage: status === RuleExecutionStatus.failed ? message : undefined, + lastSuccessAt: status !== RuleExecutionStatus.failed ? statusDate : undefined, + lastSuccessMessage: status !== RuleExecutionStatus.failed ? message : undefined, + lastLookBackDate: undefined, + gap: undefined, + bulkCreateTimeDurations: undefined, + searchAfterTimeDurations: undefined, + }; + }); } public logExecutionMetrics({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts index 7ae2f179f9692..aafacdd975e7f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts @@ -6,7 +6,8 @@ */ import { SavedObjectsClientContract } from '../../../../../../../src/core/server'; -import { IEventLogService } from '../../../../../event_log/server'; +import { IEventLogClient, IEventLogService } from '../../../../../event_log/server'; +import { IRuleStatusSOAttributes } from '../rules/types'; import { EventLogAdapter } from './event_log_adapter/event_log_adapter'; import { SavedObjectsAdapter } from './saved_objects_adapter/saved_objects_adapter'; import { @@ -15,58 +16,61 @@ import { FindExecutionLogArgs, IRuleExecutionLogClient, LogStatusChangeArgs, - UpdateExecutionLogArgs, UnderlyingLogClient, + GetLastFailuresArgs, + GetCurrentStatusArgs, + GetCurrentStatusBulkArgs, + GetCurrentStatusBulkResult, } from './types'; import { truncateMessage } from './utils/normalization'; -export interface RuleExecutionLogClientArgs { - savedObjectsClient: SavedObjectsClientContract; - eventLogService: IEventLogService; +interface ConstructorParams { underlyingClient: UnderlyingLogClient; + eventLogService: IEventLogService; + eventLogClient: IEventLogClient; + savedObjectsClient: SavedObjectsClientContract; } export class RuleExecutionLogClient implements IRuleExecutionLogClient { private client: IRuleExecutionLogClient; - constructor({ - savedObjectsClient, - eventLogService, - underlyingClient, - }: RuleExecutionLogClientArgs) { + constructor(params: ConstructorParams) { + const { underlyingClient, eventLogService, eventLogClient, savedObjectsClient } = params; + switch (underlyingClient) { case UnderlyingLogClient.savedObjects: this.client = new SavedObjectsAdapter(savedObjectsClient); break; case UnderlyingLogClient.eventLog: - this.client = new EventLogAdapter(eventLogService, savedObjectsClient); + this.client = new EventLogAdapter(eventLogService, eventLogClient, savedObjectsClient); break; } } + /** @deprecated */ public find(args: FindExecutionLogArgs) { return this.client.find(args); } + /** @deprecated */ public findBulk(args: FindBulkExecutionLogArgs) { return this.client.findBulk(args); } - public async update(args: UpdateExecutionLogArgs) { - const { lastFailureMessage, lastSuccessMessage, ...restAttributes } = args.attributes; + public getLastFailures(args: GetLastFailuresArgs): Promise { + return this.client.getLastFailures(args); + } - return this.client.update({ - ...args, - attributes: { - lastFailureMessage: truncateMessage(lastFailureMessage), - lastSuccessMessage: truncateMessage(lastSuccessMessage), - ...restAttributes, - }, - }); + public getCurrentStatus(args: GetCurrentStatusArgs): Promise { + return this.client.getCurrentStatus(args); + } + + public getCurrentStatusBulk(args: GetCurrentStatusBulkArgs): Promise { + return this.client.getCurrentStatusBulk(args); } - public async delete(id: string) { - return this.client.delete(id); + public deleteCurrentStatus(ruleId: string): Promise { + return this.client.deleteCurrentStatus(ruleId); } public async logExecutionMetrics(args: LogExecutionMetricsArgs) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts index 70db3a768fdb1..58060742f4b55 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { mapValues } from 'lodash'; import { SavedObject, SavedObjectReference } from 'src/core/server'; import { SavedObjectsClientContract } from '../../../../../../../../src/core/server'; import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; @@ -23,7 +24,10 @@ import { IRuleExecutionLogClient, ExecutionMetrics, LogStatusChangeArgs, - UpdateExecutionLogArgs, + GetLastFailuresArgs, + GetCurrentStatusArgs, + GetCurrentStatusBulkArgs, + GetCurrentStatusBulkResult, } from '../types'; import { assertUnreachable } from '../../../../../common'; @@ -48,26 +52,53 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient { this.ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); } - public find({ ruleId, logsCount = 1 }: FindExecutionLogArgs) { + private findRuleStatusSavedObjects(ruleId: string, count: number) { return this.ruleStatusClient.find({ - perPage: logsCount, + perPage: count, sortField: 'statusDate', sortOrder: 'desc', ruleId, }); } + /** @deprecated */ + public find({ ruleId, logsCount = 1 }: FindExecutionLogArgs) { + return this.findRuleStatusSavedObjects(ruleId, logsCount); + } + + /** @deprecated */ public findBulk({ ruleIds, logsCount = 1 }: FindBulkExecutionLogArgs) { return this.ruleStatusClient.findBulk(ruleIds, logsCount); } - public async update({ id, attributes, ruleId }: UpdateExecutionLogArgs) { - const references: SavedObjectReference[] = [legacyGetRuleReference(ruleId)]; - await this.ruleStatusClient.update(id, attributes, { references }); + public async getLastFailures(args: GetLastFailuresArgs): Promise { + const result = await this.findRuleStatusSavedObjects(args.ruleId, MAX_RULE_STATUSES); + + // The first status is always the current one followed by 5 last failures. + // We skip the current status and return only the failures. + return result.map((so) => so.attributes).slice(1); + } + + public async getCurrentStatus(args: GetCurrentStatusArgs): Promise { + const result = await this.findRuleStatusSavedObjects(args.ruleId, 1); + return result[0].attributes; } - public async delete(id: string) { - await this.ruleStatusClient.delete(id); + public async getCurrentStatusBulk( + args: GetCurrentStatusBulkArgs + ): Promise { + const { ruleIds } = args; + const result = await this.ruleStatusClient.findBulk(ruleIds, 1); + + return mapValues(result, (value) => { + const arrayOfAttributes = value ?? []; + return arrayOfAttributes[0]; + }); + } + + public async deleteCurrentStatus(ruleId: string): Promise { + const statusSavedObjects = await this.findRuleStatusSavedObjects(ruleId, MAX_RULE_STATUSES); + await Promise.all(statusSavedObjects.map((so) => this.ruleStatusClient.delete(so.id))); } public async logExecutionMetrics({ ruleId, metrics }: LogExecutionMetricsArgs) { @@ -109,16 +140,12 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient { private getOrCreateRuleStatuses = async ( ruleId: string ): Promise>> => { - const ruleStatuses = await this.find({ - spaceId: '', // spaceId is a required argument but it's not used by savedObjectsClient, any string would work here - ruleId, - logsCount: MAX_RULE_STATUSES, - }); - if (ruleStatuses.length > 0) { - return ruleStatuses; + const existingStatuses = await this.findRuleStatusSavedObjects(ruleId, MAX_RULE_STATUSES); + if (existingStatuses.length > 0) { + return existingStatuses; } - const newStatus = await this.createNewRuleStatus(ruleId); + const newStatus = await this.createNewRuleStatus(ruleId); return [newStatus]; }; @@ -159,7 +186,7 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient { // drop oldest failures const oldStatuses = [lastStatus, ...ruleStatuses].slice(MAX_RULE_STATUSES); - await Promise.all(oldStatuses.map((status) => this.delete(status.id))); + await Promise.all(oldStatuses.map((status) => this.ruleStatusClient.delete(status.id))); return; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts index 564145cfc5d1f..252a1fc947fd2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts @@ -15,74 +15,92 @@ export enum UnderlyingLogClient { 'eventLog' = 'eventLog', } +export interface IRuleExecutionLogClient { + /** @deprecated */ + find(args: FindExecutionLogArgs): Promise>>; + /** @deprecated */ + findBulk(args: FindBulkExecutionLogArgs): Promise; + + getLastFailures(args: GetLastFailuresArgs): Promise; + getCurrentStatus(args: GetCurrentStatusArgs): Promise; + getCurrentStatusBulk(args: GetCurrentStatusBulkArgs): Promise; + + deleteCurrentStatus(ruleId: string): Promise; + + logStatusChange(args: LogStatusChangeArgs): Promise; + logExecutionMetrics(args: LogExecutionMetricsArgs): Promise; +} + +/** @deprecated */ export interface FindExecutionLogArgs { ruleId: string; spaceId: string; logsCount?: number; } +/** @deprecated */ export interface FindBulkExecutionLogArgs { ruleIds: string[]; spaceId: string; logsCount?: number; } -export interface ExecutionMetrics { - searchDurations?: string[]; - indexingDurations?: string[]; - /** - * @deprecated lastLookBackDate is logged only by SavedObjectsAdapter and should be removed in the future - */ - lastLookBackDate?: string; - executionGap?: Duration; +/** @deprecated */ +export interface FindBulkExecutionLogResponse { + [ruleId: string]: IRuleStatusSOAttributes[] | undefined; } -export interface LogStatusChangeArgs { +export interface GetLastFailuresArgs { ruleId: string; - ruleName: string; - ruleType: string; spaceId: string; - newStatus: RuleExecutionStatus; - message?: string; - /** - * @deprecated Use RuleExecutionLogClient.logExecutionMetrics to write metrics instead - */ - metrics?: ExecutionMetrics; } -export interface UpdateExecutionLogArgs { - id: string; - attributes: IRuleStatusSOAttributes; +export interface GetCurrentStatusArgs { ruleId: string; - ruleName: string; - ruleType: string; spaceId: string; } +export interface GetCurrentStatusBulkArgs { + ruleIds: string[]; + spaceId: string; +} + +export interface GetCurrentStatusBulkResult { + [ruleId: string]: IRuleStatusSOAttributes; +} + export interface CreateExecutionLogArgs { attributes: IRuleStatusSOAttributes; spaceId: string; } -export interface LogExecutionMetricsArgs { +export interface LogStatusChangeArgs { ruleId: string; ruleName: string; ruleType: string; spaceId: string; - metrics: ExecutionMetrics; + newStatus: RuleExecutionStatus; + message?: string; + /** + * @deprecated Use RuleExecutionLogClient.logExecutionMetrics to write metrics instead + */ + metrics?: ExecutionMetrics; } -export interface FindBulkExecutionLogResponse { - [ruleId: string]: IRuleStatusSOAttributes[] | undefined; +export interface LogExecutionMetricsArgs { + ruleId: string; + ruleName: string; + ruleType: string; + spaceId: string; + metrics: ExecutionMetrics; } -export interface IRuleExecutionLogClient { - find: ( - args: FindExecutionLogArgs - ) => Promise>>; - findBulk: (args: FindBulkExecutionLogArgs) => Promise; - update: (args: UpdateExecutionLogArgs) => Promise; - delete: (id: string) => Promise; - logStatusChange: (args: LogStatusChangeArgs) => Promise; - logExecutionMetrics: (args: LogExecutionMetricsArgs) => Promise; +export interface ExecutionMetrics { + searchDurations?: string[]; + indexingDurations?: string[]; + /** + * @deprecated lastLookBackDate is logged only by SavedObjectsAdapter and should be removed in the future + */ + lastLookBackDate?: string; + executionGap?: Duration; }