Cleanup after ExecLog integration (#107695)

This commit is contained in:
Dmitry Shevchenko 2021-08-24 17:37:29 +02:00 committed by GitHub
parent dc9da8ebe0
commit 1f73c0fcfa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 579 additions and 888 deletions

View file

@ -8,6 +8,14 @@
import * as t from 'io-ts';
/**
* Converts string value to a Typescript enum
* - "foo" -> MyEnum.foo
*
* @param name Enum name
* @param originalEnum Typescript enum
* @returns Codec
*/
export function enumeration<EnumType extends string>(
name: string,
originalEnum: Record<string, EnumType>

View file

@ -6,7 +6,7 @@
*/
export class InvariantError extends Error {
name = 'Invariant violation';
name = 'InvariantError';
}
/**

View file

@ -14,14 +14,14 @@ import {
import { rulesClientMock } from '../../../../../../alerting/server/mocks';
import { licensingMock } from '../../../../../../licensing/server/mocks';
import { siemMock } from '../../../../mocks';
import { RuleExecutionLogClient } from '../../rule_execution_log/__mocks__/rule_execution_log_client';
import { ruleExecutionLogClientMock } from '../../rule_execution_log/__mocks__/rule_execution_log_client';
const createMockClients = () => ({
rulesClient: rulesClientMock.create(),
licensing: { license: licensingMock.createLicenseMock() },
clusterClient: elasticsearchServiceMock.createScopedClusterClient(),
savedObjectsClient: savedObjectsClientMock.create(),
ruleExecutionLogClient: new RuleExecutionLogClient(),
ruleExecutionLogClient: ruleExecutionLogClientMock.create(),
appClient: siemMock.createClient(),
});

View file

@ -17,7 +17,6 @@ import {
} from '../__mocks__/request_responses';
import { findRulesRoute } from './find_rules_route';
jest.mock('../../signals/rule_status_service');
describe('find_rules', () => {
let server: ReturnType<typeof serverMock.create>;
let { clients, context } = requestContextMock.createTools();

View file

@ -17,8 +17,6 @@ import { RuleStatusResponse } from '../../rules/types';
import { AlertExecutionStatusErrorReasons } from '../../../../../../alerting/common';
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
jest.mock('../../signals/rule_status_service');
describe('find_statuses', () => {
let server: ReturnType<typeof serverMock.create>;
let { clients, context } = requestContextMock.createTools();

View file

@ -7,16 +7,17 @@
import { IRuleExecutionLogClient } from '../types';
export const ruleExecutionLogClientMock = {
create: (): jest.Mocked<IRuleExecutionLogClient> => ({
find: jest.fn(),
findBulk: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
logStatusChange: jest.fn(),
logExecutionMetric: jest.fn(),
}),
};
export const RuleExecutionLogClient = jest
.fn<jest.Mocked<IRuleExecutionLogClient>, []>()
.mockImplementation(() => {
return {
find: jest.fn(),
findBulk: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
logStatusChange: jest.fn(),
logExecutionMetric: jest.fn(),
};
});
.mockImplementation(ruleExecutionLogClientMock.create);

View file

@ -1,64 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObjectsClientContract } from '../../../../../../../../src/core/server';
import {
RuleStatusSavedObjectsClient,
ruleStatusSavedObjectsClientFactory,
} from '../../signals/rule_status_saved_objects_client';
import {
CreateExecutionLogArgs,
ExecutionMetric,
ExecutionMetricArgs,
FindBulkExecutionLogArgs,
FindExecutionLogArgs,
IRuleExecutionLogClient,
LogStatusChangeArgs,
UpdateExecutionLogArgs,
} from '../types';
export class SavedObjectsAdapter implements IRuleExecutionLogClient {
private ruleStatusClient: RuleStatusSavedObjectsClient;
constructor(savedObjectsClient: SavedObjectsClientContract) {
this.ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
}
public find({ ruleId, logsCount = 1 }: FindExecutionLogArgs) {
return this.ruleStatusClient.find({
perPage: logsCount,
sortField: 'statusDate',
sortOrder: 'desc',
search: ruleId,
searchFields: ['alertId'],
});
}
public findBulk({ ruleIds, logsCount = 1 }: FindBulkExecutionLogArgs) {
return this.ruleStatusClient.findBulk(ruleIds, logsCount);
}
public async create({ attributes }: CreateExecutionLogArgs) {
return this.ruleStatusClient.create(attributes);
}
public async update({ id, attributes }: UpdateExecutionLogArgs) {
await this.ruleStatusClient.update(id, attributes);
}
public async delete(id: string) {
await this.ruleStatusClient.delete(id);
}
public async logExecutionMetric<T extends ExecutionMetric>(args: ExecutionMetricArgs<T>) {
// 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
}
}

View file

@ -6,10 +6,9 @@
*/
import { SavedObjectsClientContract } from '../../../../../../../src/core/server';
import { RuleRegistryAdapter } from './adapters/rule_registry_adapter';
import { SavedObjectsAdapter } from './adapters/saved_objects_adapter';
import { RuleRegistryAdapter } from './rule_registry_adapter/rule_registry_adapter';
import { SavedObjectsAdapter } from './saved_objects_adapter/saved_objects_adapter';
import {
CreateExecutionLogArgs,
ExecutionMetric,
ExecutionMetricArgs,
FindBulkExecutionLogArgs,
@ -46,10 +45,6 @@ export class RuleExecutionLogClient implements IRuleExecutionLogClient {
return this.client.findBulk(args);
}
public async create(args: CreateExecutionLogArgs) {
return this.client.create(args);
}
public async update(args: UpdateExecutionLogArgs) {
return this.client.update(args);
}

View file

@ -7,7 +7,7 @@
import { merge } from 'lodash';
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
import { RuleRegistryLogClient } from '../rule_registry_log_client/rule_registry_log_client';
import { RuleRegistryLogClient } from './rule_registry_log_client/rule_registry_log_client';
import {
CreateExecutionLogArgs,
ExecutionMetric,
@ -59,7 +59,7 @@ export class RuleRegistryAdapter implements IRuleExecutionLogClient {
return merge(statusesById, lastErrorsById);
}
public async create({ attributes, spaceId }: CreateExecutionLogArgs) {
private async create({ attributes, spaceId }: CreateExecutionLogArgs) {
if (attributes.status) {
await this.ruleRegistryClient.logStatusChange({
ruleId: attributes.alertId,
@ -85,14 +85,6 @@ export class RuleRegistryAdapter implements IRuleExecutionLogClient {
spaceId,
});
}
return {
id: '',
type: '',
score: 0,
attributes,
references: [],
};
}
public async update({ attributes, spaceId }: UpdateExecutionLogArgs) {

View file

@ -7,11 +7,11 @@
import { isLeft } from 'fp-ts/lib/Either';
import { PathReporter } from 'io-ts/lib/PathReporter';
import { technicalRuleFieldMap } from '../../../../../../rule_registry/common/assets/field_maps/technical_rule_field_map';
import { technicalRuleFieldMap } from '../../../../../../../rule_registry/common/assets/field_maps/technical_rule_field_map';
import {
mergeFieldMaps,
runtimeTypeFromFieldMap,
} from '../../../../../../rule_registry/common/field_map';
} from '../../../../../../../rule_registry/common/field_map';
import { ruleExecutionFieldMap } from './rule_execution_field_map';
const ruleExecutionLogRuntimeType = runtimeTypeFromFieldMap(

View file

@ -17,19 +17,19 @@ import {
} from '@kbn/rule-data-utils';
import moment from 'moment';
import { mappingFromFieldMap } from '../../../../../../rule_registry/common/mapping_from_field_map';
import { Dataset, IRuleDataClient } from '../../../../../../rule_registry/server';
import { SERVER_APP_ID } from '../../../../../common/constants';
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
import { invariant } from '../../../../../common/utils/invariant';
import { IRuleStatusSOAttributes } from '../../rules/types';
import { makeFloatString } from '../../signals/utils';
import { mappingFromFieldMap } from '../../../../../../../rule_registry/common/mapping_from_field_map';
import { Dataset, IRuleDataClient } from '../../../../../../../rule_registry/server';
import { SERVER_APP_ID } from '../../../../../../common/constants';
import { RuleExecutionStatus } from '../../../../../../common/detection_engine/schemas/common/schemas';
import { invariant } from '../../../../../../common/utils/invariant';
import { IRuleStatusSOAttributes } from '../../../rules/types';
import { makeFloatString } from '../../../signals/utils';
import {
ExecutionMetric,
ExecutionMetricArgs,
IRuleDataPluginService,
LogStatusChangeArgs,
} from '../types';
} from '../../types';
import { EVENT_SEQUENCE, MESSAGE, RULE_STATUS, RULE_STATUS_SEVERITY } from './constants';
import { parseRuleExecutionLog, RuleExecutionEvent } from './parse_rule_execution_log';
import { ruleExecutionFieldMap } from './rule_execution_field_map';

View file

@ -7,8 +7,8 @@
import { SearchSort } from '@elastic/elasticsearch/api/types';
import { EVENT_ACTION, TIMESTAMP } from '@kbn/rule-data-utils';
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
import { ExecutionMetric } from '../types';
import { RuleExecutionStatus } from '../../../../../../common/detection_engine/schemas/common/schemas';
import { ExecutionMetric } from '../../types';
import { RULE_STATUS, EVENT_SEQUENCE, EVENT_DURATION, EVENT_END } from './constants';
const METRIC_FIELDS = {

View file

@ -12,10 +12,10 @@ import {
SavedObjectsUpdateResponse,
SavedObjectsFindOptions,
SavedObjectsFindResult,
} from '../../../../../../../src/core/server';
import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings';
import { IRuleStatusSOAttributes } from '../rules/types';
import { buildChunkedOrFilter } from './utils';
} from '../../../../../../../../src/core/server';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
import { IRuleStatusSOAttributes } from '../../rules/types';
import { buildChunkedOrFilter } from '../../signals/utils';
export interface RuleStatusSavedObjectsClient {
find: (

View file

@ -0,0 +1,192 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObject } from 'src/core/server';
import { SavedObjectsClientContract } from '../../../../../../../../src/core/server';
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
import { IRuleStatusSOAttributes } from '../../rules/types';
import {
RuleStatusSavedObjectsClient,
ruleStatusSavedObjectsClientFactory,
} from './rule_status_saved_objects_client';
import {
ExecutionMetric,
ExecutionMetricArgs,
FindBulkExecutionLogArgs,
FindExecutionLogArgs,
IRuleExecutionLogClient,
LegacyMetrics,
LogStatusChangeArgs,
UpdateExecutionLogArgs,
} from '../types';
import { assertUnreachable } from '../../../../../common';
// 1st is mutable status, followed by 5 most recent failures
export const MAX_RULE_STATUSES = 6;
const METRIC_FIELDS = {
[ExecutionMetric.executionGap]: 'gap',
[ExecutionMetric.searchDurationMax]: 'searchAfterTimeDurations',
[ExecutionMetric.indexingDurationMax]: 'bulkCreateTimeDurations',
[ExecutionMetric.indexingLookback]: 'lastLookBackDate',
} as const;
const getMetricField = <T extends ExecutionMetric>(metric: T) => METRIC_FIELDS[metric];
export class SavedObjectsAdapter implements IRuleExecutionLogClient {
private ruleStatusClient: RuleStatusSavedObjectsClient;
constructor(savedObjectsClient: SavedObjectsClientContract) {
this.ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
}
public find({ ruleId, logsCount = 1 }: FindExecutionLogArgs) {
return this.ruleStatusClient.find({
perPage: logsCount,
sortField: 'statusDate',
sortOrder: 'desc',
search: ruleId,
searchFields: ['alertId'],
});
}
public findBulk({ ruleIds, logsCount = 1 }: FindBulkExecutionLogArgs) {
return this.ruleStatusClient.findBulk(ruleIds, logsCount);
}
public async update({ id, attributes }: UpdateExecutionLogArgs) {
await this.ruleStatusClient.update(id, attributes);
}
public async delete(id: string) {
await this.ruleStatusClient.delete(id);
}
public async logExecutionMetric<T extends ExecutionMetric>({
ruleId,
metric,
value,
}: ExecutionMetricArgs<T>) {
const [currentStatus] = await this.getOrCreateRuleStatuses(ruleId);
await this.ruleStatusClient.update(currentStatus.id, {
...currentStatus.attributes,
[getMetricField(metric)]: value,
});
}
private createNewRuleStatus = async (
ruleId: string
): Promise<SavedObject<IRuleStatusSOAttributes>> => {
const now = new Date().toISOString();
return this.ruleStatusClient.create({
alertId: ruleId,
statusDate: now,
status: RuleExecutionStatus['going to run'],
lastFailureAt: null,
lastSuccessAt: null,
lastFailureMessage: null,
lastSuccessMessage: null,
gap: null,
bulkCreateTimeDurations: [],
searchAfterTimeDurations: [],
lastLookBackDate: null,
});
};
private getOrCreateRuleStatuses = async (
ruleId: string
): Promise<Array<SavedObject<IRuleStatusSOAttributes>>> => {
const ruleStatuses = await this.find({
spaceId: '', // spaceId is a required argument but it's not used by savedObjectsClient, any string would work here
ruleId,
logsCount: MAX_RULE_STATUSES,
});
if (ruleStatuses.length > 0) {
return ruleStatuses;
}
const newStatus = await this.createNewRuleStatus(ruleId);
return [newStatus];
};
public async logStatusChange({ newStatus, ruleId, message, metrics }: LogStatusChangeArgs) {
switch (newStatus) {
case RuleExecutionStatus['going to run']:
case RuleExecutionStatus.succeeded:
case RuleExecutionStatus.warning:
case RuleExecutionStatus['partial failure']: {
const [currentStatus] = await this.getOrCreateRuleStatuses(ruleId);
await this.ruleStatusClient.update(currentStatus.id, {
...currentStatus.attributes,
...buildRuleStatusAttributes(newStatus, message, metrics),
});
return;
}
case RuleExecutionStatus.failed: {
const ruleStatuses = await this.getOrCreateRuleStatuses(ruleId);
const [currentStatus] = ruleStatuses;
const failureAttributes = {
...currentStatus.attributes,
...buildRuleStatusAttributes(RuleExecutionStatus.failed, message, metrics),
};
// We always update the newest status, so to 'persist' a failure we push a copy to the head of the list
await this.ruleStatusClient.update(currentStatus.id, failureAttributes);
const lastStatus = await this.ruleStatusClient.create(failureAttributes);
// drop oldest failures
const oldStatuses = [lastStatus, ...ruleStatuses].slice(MAX_RULE_STATUSES);
await Promise.all(oldStatuses.map((status) => this.delete(status.id)));
return;
}
default:
assertUnreachable(newStatus, 'Unknown rule execution status supplied to logStatusChange');
}
}
}
const buildRuleStatusAttributes: (
status: RuleExecutionStatus,
message?: string,
metrics?: LegacyMetrics
) => Partial<IRuleStatusSOAttributes> = (status, message, metrics = {}) => {
const now = new Date().toISOString();
const baseAttributes: Partial<IRuleStatusSOAttributes> = {
...metrics,
status:
status === RuleExecutionStatus.warning ? RuleExecutionStatus['partial failure'] : status,
statusDate: now,
};
switch (status) {
case RuleExecutionStatus.succeeded:
case RuleExecutionStatus.warning:
case RuleExecutionStatus['partial failure']: {
return {
...baseAttributes,
lastSuccessAt: now,
lastSuccessMessage: message,
};
}
case RuleExecutionStatus.failed: {
return {
...baseAttributes,
lastFailureAt: now,
lastFailureMessage: message,
};
}
case RuleExecutionStatus['going to run']: {
return baseAttributes;
}
}
};

View file

@ -6,7 +6,7 @@
*/
import { PublicMethodsOf } from '@kbn/utility-types';
import { SavedObject, SavedObjectsFindResult } from '../../../../../../../src/core/server';
import { SavedObjectsFindResult } from '../../../../../../../src/core/server';
import { RuleDataPluginService } from '../../../../../rule_registry/server';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
import { IRuleStatusSOAttributes } from '../rules/types';
@ -39,12 +39,24 @@ export interface FindBulkExecutionLogArgs {
logsCount?: number;
}
/**
* @deprecated LegacyMetrics are only kept here for backward compatibility
* and should be replaced by ExecutionMetric in the future
*/
export interface LegacyMetrics {
searchAfterTimeDurations?: string[];
bulkCreateTimeDurations?: string[];
lastLookBackDate?: string;
gap?: string;
}
export interface LogStatusChangeArgs {
ruleId: string;
spaceId: string;
newStatus: RuleExecutionStatus;
namespace?: string;
message?: string;
metrics?: LegacyMetrics;
}
export interface UpdateExecutionLogArgs {
@ -75,10 +87,8 @@ export interface IRuleExecutionLogClient {
args: FindExecutionLogArgs
) => Promise<Array<SavedObjectsFindResult<IRuleStatusSOAttributes>>>;
findBulk: (args: FindBulkExecutionLogArgs) => Promise<FindBulkExecutionLogResponse>;
create: (args: CreateExecutionLogArgs) => Promise<SavedObject<IRuleStatusSOAttributes>>;
update: (args: UpdateExecutionLogArgs) => Promise<void>;
delete: (id: string) => Promise<void>;
// TODO These methods are intended to supersede ones provided by RuleStatusService
logStatusChange: (args: LogStatusChangeArgs) => Promise<void>;
logExecutionMetric: <T extends ExecutionMetric>(args: ExecutionMetricArgs<T>) => Promise<void>;
}

View file

@ -1,80 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Logger } from '@kbn/logging';
import {
AlertInstanceContext,
AlertTypeParams,
AlertTypeState,
} from '../../../../../alerting/common';
import { AlertTypeWithExecutor } from '../../../../../rule_registry/server';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
import { RuleExecutionLogClient } from './rule_execution_log_client';
import { IRuleDataPluginService, IRuleExecutionLogClient } from './types';
export interface ExecutionLogServices {
ruleExecutionLogClient: IRuleExecutionLogClient;
logger: Logger;
}
type WithRuleExecutionLog = (args: {
logger: Logger;
ruleDataService: IRuleDataPluginService;
}) => <
TState extends AlertTypeState,
TParams extends AlertTypeParams,
TAlertInstanceContext extends AlertInstanceContext,
TServices extends ExecutionLogServices
>(
type: AlertTypeWithExecutor<TState, TParams, TAlertInstanceContext, TServices>
) => AlertTypeWithExecutor<TState, TParams, TAlertInstanceContext, TServices>;
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,
});
}
},
};
};

View file

@ -12,7 +12,6 @@ import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils';
import { ListArray } from '@kbn/securitysolution-io-ts-list-types';
import { toError } from '@kbn/securitysolution-list-api';
import { createPersistenceRuleTypeFactory } from '../../../../../rule_registry/server';
import { ruleStatusServiceFactory } from '../signals/rule_status_service';
import { buildRuleMessageFactory } from './factories/build_rule_message_factory';
import {
checkPrivilegesFromEsClient,
@ -33,6 +32,7 @@ import { getNotificationResultsLink } from '../notifications/utils';
import { createResultObject } from './utils';
import { bulkCreateFactory, wrapHitsFactory } from './factories';
import { RuleExecutionLogClient } from '../rule_execution_log/rule_execution_log_client';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
/* eslint-disable complexity */
export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({
@ -63,12 +63,6 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({
const esClient = scopedClusterClient.asCurrentUser;
const ruleStatusClient = new RuleExecutionLogClient({ savedObjectsClient, ruleDataService });
const ruleStatusService = await ruleStatusServiceFactory({
spaceId,
alertId,
ruleStatusClient,
});
const ruleSO = await savedObjectsClient.get('alert', alertId);
const {
@ -89,7 +83,11 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({
logger.debug(buildRuleMessage(`interval: ${interval}`));
let wroteWarningStatus = false;
await ruleStatusService.goingToRun();
await ruleStatusClient.logStatusChange({
spaceId,
ruleId: alertId,
newStatus: RuleExecutionStatus['going to run'],
});
let result = createResultObject(state);
@ -122,22 +120,33 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({
() =>
tryCatch(
() =>
hasReadIndexPrivileges(privileges, logger, buildRuleMessage, ruleStatusService),
hasReadIndexPrivileges({
spaceId,
ruleId: alertId,
privileges,
logger,
buildRuleMessage,
ruleStatusClient,
}),
toError
),
chain((wroteStatus: unknown) =>
tryCatch(
() =>
hasTimestampFields(
wroteStatus as boolean,
hasTimestampOverride ? (timestampOverride as string) : '@timestamp',
name,
timestampFieldCaps,
hasTimestampFields({
spaceId,
ruleId: alertId,
wroteStatus: wroteStatus as boolean,
timestampField: hasTimestampOverride
? (timestampOverride as string)
: '@timestamp',
ruleName: name,
timestampFieldCapsResponse: timestampFieldCaps,
inputIndices,
ruleStatusService,
ruleStatusClient,
logger,
buildRuleMessage
),
buildRuleMessage,
}),
toError
)
)
@ -165,7 +174,13 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({
);
logger.warn(gapMessage);
hasError = true;
await ruleStatusService.error(gapMessage, { gap: gapString });
await ruleStatusClient.logStatusChange({
spaceId,
ruleId: alertId,
newStatus: RuleExecutionStatus.failed,
message: gapMessage,
metrics: { gap: gapString },
});
}
try {
@ -232,7 +247,12 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({
if (result.warningMessages.length) {
const warningMessage = buildRuleMessage(result.warningMessages.join());
await ruleStatusService.partialFailure(warningMessage);
await ruleStatusClient.logStatusChange({
spaceId,
ruleId: alertId,
newStatus: RuleExecutionStatus['partial failure'],
message: warningMessage,
});
}
if (result.success) {
@ -277,10 +297,16 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({
);
if (!hasError && !wroteWarningStatus && !result.warning) {
await ruleStatusService.success('succeeded', {
bulkCreateTimeDurations: result.bulkCreateTimes,
searchAfterTimeDurations: result.searchAfterTimes,
lastLookBackDate: result.lastLookbackDate?.toISOString(),
await ruleStatusClient.logStatusChange({
spaceId,
ruleId: alertId,
newStatus: RuleExecutionStatus.succeeded,
message: 'succeeded',
metrics: {
bulkCreateTimeDurations: result.bulkCreateTimes,
searchAfterTimeDurations: result.searchAfterTimes,
lastLookBackDate: result.lastLookbackDate?.toISOString(),
},
});
}
@ -300,10 +326,16 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({
result.errors.join()
);
logger.error(errorMessage);
await ruleStatusService.error(errorMessage, {
bulkCreateTimeDurations: result.bulkCreateTimes,
searchAfterTimeDurations: result.searchAfterTimes,
lastLookBackDate: result.lastLookbackDate?.toISOString(),
await ruleStatusClient.logStatusChange({
spaceId,
ruleId: alertId,
newStatus: RuleExecutionStatus.failed,
message: errorMessage,
metrics: {
bulkCreateTimeDurations: result.bulkCreateTimes,
searchAfterTimeDurations: result.searchAfterTimes,
lastLookBackDate: result.lastLookbackDate?.toISOString(),
},
});
}
} catch (error) {
@ -314,10 +346,16 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({
);
logger.error(message);
await ruleStatusService.error(message, {
bulkCreateTimeDurations: result.bulkCreateTimes,
searchAfterTimeDurations: result.searchAfterTimes,
lastLookBackDate: result.lastLookbackDate?.toISOString(),
await ruleStatusClient.logStatusChange({
spaceId,
ruleId: alertId,
newStatus: RuleExecutionStatus.failed,
message,
metrics: {
bulkCreateTimeDurations: result.bulkCreateTimes,
searchAfterTimeDurations: result.searchAfterTimes,
lastLookBackDate: result.lastLookbackDate?.toISOString(),
},
});
}

View file

@ -26,14 +26,7 @@ jest.mock('../utils/get_list_client', () => ({
}),
}));
jest.mock('../../signals/rule_status_service', () => ({
ruleStatusServiceFactory: () => ({
goingToRun: jest.fn(),
success: jest.fn(),
partialFailure: jest.fn(),
error: jest.fn(),
}),
}));
jest.mock('../../rule_execution_log/rule_execution_log_client');
describe('Indicator Match Alerts', () => {
const params: Partial<RuleParams> = {

View file

@ -22,14 +22,7 @@ jest.mock('../utils/get_list_client', () => ({
}),
}));
jest.mock('../../signals/rule_status_service', () => ({
ruleStatusServiceFactory: () => ({
goingToRun: jest.fn(),
success: jest.fn(),
partialFailure: jest.fn(),
error: jest.fn(),
}),
}));
jest.mock('../../rule_execution_log/rule_execution_log_client');
describe('Custom query alerts', () => {
it('does not send an alert when no events found', async () => {

View file

@ -12,20 +12,20 @@ import { deleteNotifications } from '../notifications/delete_notifications';
import { deleteRuleActionsSavedObject } from '../rule_actions/delete_rule_actions_saved_object';
import { SavedObjectsFindResult } from '../../../../../../../src/core/server';
import { IRuleStatusSOAttributes } from './types';
import { RuleExecutionLogClient } from '../rule_execution_log/__mocks__/rule_execution_log_client';
import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client';
jest.mock('../notifications/delete_notifications');
jest.mock('../rule_actions/delete_rule_actions_saved_object');
describe('deleteRules', () => {
let rulesClient: ReturnType<typeof rulesClientMock.create>;
let ruleStatusClient: ReturnType<typeof RuleExecutionLogClient>;
let ruleStatusClient: ReturnType<typeof ruleExecutionLogClientMock.create>;
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
beforeEach(() => {
rulesClient = rulesClientMock.create();
savedObjectsClient = savedObjectsClientMock.create();
ruleStatusClient = new RuleExecutionLogClient();
ruleStatusClient = ruleExecutionLogClientMock.create();
});
it('should delete the rule along with its notifications, actions, and statuses', async () => {

View file

@ -9,14 +9,14 @@ import { PatchRulesOptions } from './types';
import { rulesClientMock } from '../../../../../alerting/server/mocks';
import { getAlertMock } from '../routes/__mocks__/request_responses';
import { getMlRuleParams, getQueryRuleParams } from '../schemas/rule_schemas.mock';
import { RuleExecutionLogClient } from '../rule_execution_log/__mocks__/rule_execution_log_client';
import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client';
export const getPatchRulesOptionsMock = (): PatchRulesOptions => ({
author: ['Elastic'],
buildingBlockType: undefined,
rulesClient: rulesClientMock.create(),
spaceId: 'default',
ruleStatusClient: new RuleExecutionLogClient(),
ruleStatusClient: ruleExecutionLogClientMock.create(),
anomalyThreshold: undefined,
description: 'some description',
enabled: true,
@ -68,7 +68,7 @@ export const getPatchMlRulesOptionsMock = (): PatchRulesOptions => ({
buildingBlockType: undefined,
rulesClient: rulesClientMock.create(),
spaceId: 'default',
ruleStatusClient: new RuleExecutionLogClient(),
ruleStatusClient: ruleExecutionLogClientMock.create(),
anomalyThreshold: 55,
description: 'some description',
enabled: true,

View file

@ -10,16 +10,16 @@ import { getFindResultWithSingleHit } from '../routes/__mocks__/request_response
import { updatePrepackagedRules } from './update_prepacked_rules';
import { patchRules } from './patch_rules';
import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock';
import { RuleExecutionLogClient } from '../rule_execution_log/__mocks__/rule_execution_log_client';
import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client';
jest.mock('./patch_rules');
describe('updatePrepackagedRules', () => {
let rulesClient: ReturnType<typeof rulesClientMock.create>;
let ruleStatusClient: ReturnType<typeof RuleExecutionLogClient>;
let ruleStatusClient: ReturnType<typeof ruleExecutionLogClientMock.create>;
beforeEach(() => {
rulesClient = rulesClientMock.create();
ruleStatusClient = new RuleExecutionLogClient();
ruleStatusClient = ruleExecutionLogClientMock.create();
});
it('should omit actions and enabled when calling patchRules', async () => {

View file

@ -10,13 +10,13 @@ import {
getUpdateMachineLearningSchemaMock,
getUpdateRulesSchemaMock,
} from '../../../../common/detection_engine/schemas/request/rule_schemas.mock';
import { RuleExecutionLogClient } from '../rule_execution_log/__mocks__/rule_execution_log_client';
import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client';
import { UpdateRulesOptions } from './types';
export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({
spaceId: 'default',
rulesClient: rulesClientMock.create(),
ruleStatusClient: new RuleExecutionLogClient(),
ruleStatusClient: ruleExecutionLogClientMock.create(),
defaultOutputIndex: '.siem-signals-default',
ruleUpdate: getUpdateRulesSchemaMock(),
});
@ -24,7 +24,7 @@ export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({
export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({
spaceId: 'default',
rulesClient: rulesClientMock.create(),
ruleStatusClient: new RuleExecutionLogClient(),
ruleStatusClient: ruleExecutionLogClientMock.create(),
defaultOutputIndex: '.siem-signals-default',
ruleUpdate: getUpdateMachineLearningSchemaMock(),
});

View file

@ -15,7 +15,7 @@ import type {
WrappedSignalHit,
AlertAttributes,
} from '../types';
import { SavedObject, SavedObjectsFindResult } from '../../../../../../../../src/core/server';
import { SavedObject } from '../../../../../../../../src/core/server';
import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks';
import { IRuleStatusSOAttributes } from '../../rules/types';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
@ -744,12 +744,6 @@ export const exampleRuleStatus: () => SavedObject<IRuleStatusSOAttributes> = ()
version: 'WzgyMiwxXQ==',
});
export const exampleFindRuleStatusResponse: (
mockStatuses: Array<SavedObject<IRuleStatusSOAttributes>>
) => Array<SavedObjectsFindResult<IRuleStatusSOAttributes>> = (
mockStatuses = [exampleRuleStatus()]
) => mockStatuses.map((obj) => ({ ...obj, score: 1 }));
export const mockLogger = loggingSystemMock.createLogger();
export const sampleBulkErrorItem = (

View file

@ -1,20 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { RuleStatusSavedObjectsClient } from '../rule_status_saved_objects_client';
const createMockRuleStatusSavedObjectsClient = (): jest.Mocked<RuleStatusSavedObjectsClient> => ({
find: jest.fn(),
findBulk: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
});
export const ruleStatusSavedObjectsClientMock = {
create: createMockRuleStatusSavedObjectsClient,
};

View file

@ -1,61 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObject } from 'src/core/server';
import { IRuleStatusSOAttributes } from '../rules/types';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
import { IRuleExecutionLogClient } from '../rule_execution_log/types';
import { MAX_RULE_STATUSES } from './rule_status_service';
interface RuleStatusParams {
alertId: string;
spaceId: string;
ruleStatusClient: IRuleExecutionLogClient;
}
export const createNewRuleStatus = async ({
alertId,
spaceId,
ruleStatusClient,
}: RuleStatusParams): Promise<SavedObject<IRuleStatusSOAttributes>> => {
const now = new Date().toISOString();
return ruleStatusClient.create({
spaceId,
attributes: {
alertId,
statusDate: now,
status: RuleExecutionStatus['going to run'],
lastFailureAt: null,
lastSuccessAt: null,
lastFailureMessage: null,
lastSuccessMessage: null,
gap: null,
bulkCreateTimeDurations: [],
searchAfterTimeDurations: [],
lastLookBackDate: null,
},
});
};
export const getOrCreateRuleStatuses = async ({
spaceId,
alertId,
ruleStatusClient,
}: RuleStatusParams): Promise<Array<SavedObject<IRuleStatusSOAttributes>>> => {
const ruleStatuses = await ruleStatusClient.find({
spaceId,
ruleId: alertId,
logsCount: MAX_RULE_STATUSES,
});
if (ruleStatuses.length > 0) {
return ruleStatuses;
}
const newStatus = await createNewRuleStatus({ alertId, spaceId, ruleStatusClient });
return [newStatus];
};

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { RuleStatusService } from './rule_status_service';
export const getRuleStatusServiceMock = (): jest.Mocked<RuleStatusService> => ({
goingToRun: jest.fn(),
success: jest.fn(),
partialFailure: jest.fn(),
error: jest.fn(),
});

View file

@ -1,238 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
buildRuleStatusAttributes,
RuleStatusService,
ruleStatusServiceFactory,
MAX_RULE_STATUSES,
} from './rule_status_service';
import { exampleRuleStatus, exampleFindRuleStatusResponse } from './__mocks__/es_results';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
import { RuleExecutionLogClient } from '../rule_execution_log/__mocks__/rule_execution_log_client';
import { UpdateExecutionLogArgs } from '../rule_execution_log/types';
const expectIsoDateString = expect.stringMatching(/2.*Z$/);
const buildStatuses = (n: number) =>
Array(n)
.fill(exampleRuleStatus())
.map((status, index) => ({
...status,
id: `status-index-${index}`,
}));
describe('buildRuleStatusAttributes', () => {
it('generates a new date on each call', async () => {
const { statusDate } = buildRuleStatusAttributes(RuleExecutionStatus['going to run']);
await new Promise((resolve) => setTimeout(resolve, 10)); // ensure time has passed
const { statusDate: statusDate2 } = buildRuleStatusAttributes(
RuleExecutionStatus['going to run']
);
expect(statusDate).toEqual(expectIsoDateString);
expect(statusDate2).toEqual(expectIsoDateString);
expect(statusDate).not.toEqual(statusDate2);
});
it('returns a status and statusDate if "going to run"', () => {
const result = buildRuleStatusAttributes(RuleExecutionStatus['going to run']);
expect(result).toEqual({
status: 'going to run',
statusDate: expectIsoDateString,
});
});
it('returns success fields if "success"', () => {
const result = buildRuleStatusAttributes(RuleExecutionStatus.succeeded, 'success message');
expect(result).toEqual({
status: 'succeeded',
statusDate: expectIsoDateString,
lastSuccessAt: expectIsoDateString,
lastSuccessMessage: 'success message',
});
expect(result.statusDate).toEqual(result.lastSuccessAt);
});
it('returns warning fields if "warning"', () => {
const result = buildRuleStatusAttributes(
RuleExecutionStatus.warning,
'some indices missing timestamp override field'
);
expect(result).toEqual({
status: 'warning',
statusDate: expectIsoDateString,
lastSuccessAt: expectIsoDateString,
lastSuccessMessage: 'some indices missing timestamp override field',
});
expect(result.statusDate).toEqual(result.lastSuccessAt);
});
it('returns failure fields if "failed"', () => {
const result = buildRuleStatusAttributes(RuleExecutionStatus.failed, 'failure message');
expect(result).toEqual({
status: 'failed',
statusDate: expectIsoDateString,
lastFailureAt: expectIsoDateString,
lastFailureMessage: 'failure message',
});
expect(result.statusDate).toEqual(result.lastFailureAt);
});
});
describe('ruleStatusService', () => {
let currentStatus: ReturnType<typeof exampleRuleStatus>;
let ruleStatusClient: ReturnType<typeof RuleExecutionLogClient>;
let service: RuleStatusService;
beforeEach(async () => {
currentStatus = exampleRuleStatus();
ruleStatusClient = new RuleExecutionLogClient();
ruleStatusClient.find.mockResolvedValue(exampleFindRuleStatusResponse([currentStatus]));
service = await ruleStatusServiceFactory({
alertId: 'mock-alert-id',
ruleStatusClient,
spaceId: 'default',
});
});
describe('goingToRun', () => {
it('updates the current status to "going to run"', async () => {
await service.goingToRun();
expect(ruleStatusClient.update).toHaveBeenCalledWith<[UpdateExecutionLogArgs]>({
id: currentStatus.id,
spaceId: 'default',
attributes: expect.objectContaining({
status: 'going to run',
statusDate: expectIsoDateString,
}),
});
});
});
describe('success', () => {
it('updates the current status to "succeeded"', async () => {
await service.success('hey, it worked');
expect(ruleStatusClient.update).toHaveBeenCalledWith<[UpdateExecutionLogArgs]>({
id: currentStatus.id,
spaceId: 'default',
attributes: expect.objectContaining({
status: 'succeeded',
statusDate: expectIsoDateString,
lastSuccessAt: expectIsoDateString,
lastSuccessMessage: 'hey, it worked',
}),
});
});
});
describe('error', () => {
beforeEach(() => {
// mock the creation of our new status
ruleStatusClient.create.mockResolvedValue(exampleRuleStatus());
});
it('updates the current status to "failed"', async () => {
await service.error('oh no, it broke');
expect(ruleStatusClient.update).toHaveBeenCalledWith<[UpdateExecutionLogArgs]>({
id: currentStatus.id,
spaceId: 'default',
attributes: expect.objectContaining({
status: 'failed',
statusDate: expectIsoDateString,
lastFailureAt: expectIsoDateString,
lastFailureMessage: 'oh no, it broke',
}),
});
});
it('does not delete statuses if we have less than the max number of statuses', async () => {
await service.error('oh no, it broke');
expect(ruleStatusClient.delete).not.toHaveBeenCalled();
});
it('does not delete rule statuses when we just hit the limit', async () => {
// max - 1 in store, meaning our new error will put us at max
ruleStatusClient.find.mockResolvedValue(
exampleFindRuleStatusResponse(buildStatuses(MAX_RULE_STATUSES - 1))
);
service = await ruleStatusServiceFactory({
alertId: 'mock-alert-id',
ruleStatusClient,
spaceId: 'default',
});
await service.error('oh no, it broke');
expect(ruleStatusClient.delete).not.toHaveBeenCalled();
});
it('deletes stale rule status when we already have max statuses', async () => {
// max in store, meaning our new error will push one off the end
ruleStatusClient.find.mockResolvedValue(
exampleFindRuleStatusResponse(buildStatuses(MAX_RULE_STATUSES))
);
service = await ruleStatusServiceFactory({
alertId: 'mock-alert-id',
ruleStatusClient,
spaceId: 'default',
});
await service.error('oh no, it broke');
expect(ruleStatusClient.delete).toHaveBeenCalledTimes(1);
// we should delete the 6th (index 5)
expect(ruleStatusClient.delete).toHaveBeenCalledWith('status-index-5');
});
it('deletes any number of rule statuses in excess of the max', async () => {
// max + 1 in store, meaning our new error will put us two over
ruleStatusClient.find.mockResolvedValue(
exampleFindRuleStatusResponse(buildStatuses(MAX_RULE_STATUSES + 1))
);
service = await ruleStatusServiceFactory({
alertId: 'mock-alert-id',
ruleStatusClient,
spaceId: 'default',
});
await service.error('oh no, it broke');
expect(ruleStatusClient.delete).toHaveBeenCalledTimes(2);
// we should delete the 6th (index 5)
expect(ruleStatusClient.delete).toHaveBeenCalledWith('status-index-5');
// we should delete the 7th (index 6)
expect(ruleStatusClient.delete).toHaveBeenCalledWith('status-index-6');
});
it('handles multiple error calls', async () => {
// max in store, meaning our new error will push one off the end
ruleStatusClient.find.mockResolvedValue(
exampleFindRuleStatusResponse(buildStatuses(MAX_RULE_STATUSES))
);
service = await ruleStatusServiceFactory({
alertId: 'mock-alert-id',
ruleStatusClient,
spaceId: 'default',
});
await service.error('oh no, it broke');
await service.error('oh no, it broke');
expect(ruleStatusClient.delete).toHaveBeenCalledTimes(2);
// we should delete the 6th (index 5)
expect(ruleStatusClient.delete).toHaveBeenCalledWith('status-index-5');
expect(ruleStatusClient.delete).toHaveBeenCalledWith('status-index-5');
});
});
});

View file

@ -1,167 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { assertUnreachable } from '../../../../common/utility_types';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
import { IRuleStatusSOAttributes } from '../rules/types';
import { getOrCreateRuleStatuses } from './get_or_create_rule_statuses';
import { IRuleExecutionLogClient } from '../rule_execution_log/types';
// 1st is mutable status, followed by 5 most recent failures
export const MAX_RULE_STATUSES = 6;
interface Attributes {
searchAfterTimeDurations?: string[];
bulkCreateTimeDurations?: string[];
lastLookBackDate?: string;
gap?: string;
}
export interface RuleStatusService {
goingToRun: () => Promise<void>;
success: (message: string, attributes?: Attributes) => Promise<void>;
partialFailure: (message: string, attributes?: Attributes) => Promise<void>;
error: (message: string, attributes?: Attributes) => Promise<void>;
}
export const buildRuleStatusAttributes: (
status: RuleExecutionStatus,
message?: string,
attributes?: Attributes
) => Partial<IRuleStatusSOAttributes> = (status, message, attributes = {}) => {
const now = new Date().toISOString();
const baseAttributes: Partial<IRuleStatusSOAttributes> = {
...attributes,
status,
statusDate: now,
};
switch (status) {
case RuleExecutionStatus.succeeded: {
return {
...baseAttributes,
lastSuccessAt: now,
lastSuccessMessage: message,
};
}
case RuleExecutionStatus.warning: {
return {
...baseAttributes,
lastSuccessAt: now,
lastSuccessMessage: message,
};
}
case RuleExecutionStatus['partial failure']: {
return {
...baseAttributes,
lastSuccessAt: now,
lastSuccessMessage: message,
};
}
case RuleExecutionStatus.failed: {
return {
...baseAttributes,
lastFailureAt: now,
lastFailureMessage: message,
};
}
case RuleExecutionStatus['going to run']: {
return baseAttributes;
}
}
assertUnreachable(status);
};
export const ruleStatusServiceFactory = async ({
spaceId,
alertId,
ruleStatusClient,
}: {
spaceId: string;
alertId: string;
ruleStatusClient: IRuleExecutionLogClient;
}): Promise<RuleStatusService> => {
return {
goingToRun: async () => {
const [currentStatus] = await getOrCreateRuleStatuses({
spaceId,
alertId,
ruleStatusClient,
});
await ruleStatusClient.update({
id: currentStatus.id,
attributes: {
...currentStatus.attributes,
...buildRuleStatusAttributes(RuleExecutionStatus['going to run']),
},
spaceId,
});
},
success: async (message, attributes) => {
const [currentStatus] = await getOrCreateRuleStatuses({
spaceId,
alertId,
ruleStatusClient,
});
await ruleStatusClient.update({
id: currentStatus.id,
attributes: {
...currentStatus.attributes,
...buildRuleStatusAttributes(RuleExecutionStatus.succeeded, message, attributes),
},
spaceId,
});
},
partialFailure: async (message, attributes) => {
const [currentStatus] = await getOrCreateRuleStatuses({
spaceId,
alertId,
ruleStatusClient,
});
await ruleStatusClient.update({
id: currentStatus.id,
attributes: {
...currentStatus.attributes,
...buildRuleStatusAttributes(RuleExecutionStatus['partial failure'], message, attributes),
},
spaceId,
});
},
error: async (message, attributes) => {
const ruleStatuses = await getOrCreateRuleStatuses({
spaceId,
alertId,
ruleStatusClient,
});
const [currentStatus] = ruleStatuses;
const failureAttributes = {
...currentStatus.attributes,
...buildRuleStatusAttributes(RuleExecutionStatus.failed, message, attributes),
};
// We always update the newest status, so to 'persist' a failure we push a copy to the head of the list
await ruleStatusClient.update({
id: currentStatus.id,
attributes: failureAttributes,
spaceId,
});
const newStatus = await ruleStatusClient.create({ attributes: failureAttributes, spaceId });
// drop oldest failures
const oldStatuses = [newStatus, ...ruleStatuses].slice(MAX_RULE_STATUSES);
await Promise.all(oldStatuses.map((status) => ruleStatusClient.delete(status.id)));
},
};
};

View file

@ -11,7 +11,6 @@ import { loggingSystemMock } from 'src/core/server/mocks';
import { getAlertMock } from '../routes/__mocks__/request_responses';
import { signalRulesAlertType } from './signal_rule_alert_type';
import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks';
import { ruleStatusServiceFactory } from './rule_status_service';
import {
getListsClient,
getExceptions,
@ -35,9 +34,9 @@ import { getMlRuleParams, getQueryRuleParams } from '../schemas/rule_schemas.moc
import { ResponseError } from '@elastic/elasticsearch/lib/errors';
import { allowedExperimentalValues } from '../../../../common/experimental_features';
import { ruleRegistryMocks } from '../../../../../rule_registry/server/mocks';
import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
jest.mock('./rule_status_saved_objects_client');
jest.mock('./rule_status_service');
jest.mock('./utils', () => {
const original = jest.requireActual('./utils');
return {
@ -59,6 +58,12 @@ jest.mock('@kbn/securitysolution-io-ts-utils', () => {
};
});
const mockRuleExecutionLogClient = ruleExecutionLogClientMock.create();
jest.mock('../rule_execution_log/rule_execution_log_client', () => ({
RuleExecutionLogClient: jest.fn().mockImplementation(() => mockRuleExecutionLogClient),
}));
const getPayload = (
ruleAlert: RuleAlertType,
services: AlertServicesMock
@ -119,21 +124,12 @@ describe('signal_rule_alert_type', () => {
let alert: ReturnType<typeof signalRulesAlertType>;
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
let alertServices: AlertServicesMock;
let ruleStatusService: Record<string, jest.Mock>;
let ruleDataService: ReturnType<typeof ruleRegistryMocks.createRuleDataPluginService>;
beforeEach(() => {
alertServices = alertsMock.createAlertServices();
logger = loggingSystemMock.createLogger();
ruleStatusService = {
success: jest.fn(),
find: jest.fn(),
goingToRun: jest.fn(),
error: jest.fn(),
partialFailure: jest.fn(),
};
ruleDataService = ruleRegistryMocks.createRuleDataPluginService();
(ruleStatusServiceFactory as jest.Mock).mockReturnValue(ruleStatusService);
(getListsClient as jest.Mock).mockReturnValue({
listClient: getListClientMock(),
exceptionsClient: getExceptionListClientMock(),
@ -201,23 +197,33 @@ describe('signal_rule_alert_type', () => {
mergeStrategy: 'missingFields',
ruleDataService,
});
mockRuleExecutionLogClient.logStatusChange.mockClear();
});
describe('executor', () => {
it('should call ruleStatusService.success if signals were created', async () => {
it('should log success status if signals were created', async () => {
payload.previousStartedAt = null;
await alert.executor(payload);
expect(ruleStatusService.success).toHaveBeenCalled();
expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenLastCalledWith(
expect.objectContaining({
newStatus: RuleExecutionStatus.succeeded,
})
);
});
it('should warn about the gap between runs if gap is very large', async () => {
payload.previousStartedAt = moment().subtract(100, 'm').toDate();
await alert.executor(payload);
expect(logger.warn).toHaveBeenCalled();
expect(ruleStatusService.error).toHaveBeenCalled();
expect(ruleStatusService.error.mock.calls[0][1]).toEqual({
gap: 'an hour',
});
expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenLastCalledWith(
expect.objectContaining({
newStatus: RuleExecutionStatus.failed,
metrics: {
gap: 'an hour',
},
})
);
});
it('should set a warning for when rules cannot read ALL provided indices', async () => {
@ -243,9 +249,12 @@ describe('signal_rule_alert_type', () => {
payload = getPayload(newRuleAlert, alertServices) as jest.Mocked<RuleExecutorOptions>;
await alert.executor(payload);
expect(ruleStatusService.partialFailure).toHaveBeenCalled();
expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain(
'Missing required read privileges on the following indices: ["some*"]'
expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
newStatus: RuleExecutionStatus['partial failure'],
message: 'Missing required read privileges on the following indices: ["some*"]',
})
);
});
@ -269,9 +278,13 @@ describe('signal_rule_alert_type', () => {
payload = getPayload(newRuleAlert, alertServices) as jest.Mocked<RuleExecutorOptions>;
await alert.executor(payload);
expect(ruleStatusService.partialFailure).toHaveBeenCalled();
expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain(
'This rule may not have the required read privileges to the following indices: ["myfa*","some*"]'
expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
newStatus: RuleExecutionStatus['partial failure'],
message:
'This rule may not have the required read privileges to the following indices: ["myfa*","some*"]',
})
);
});
@ -279,7 +292,19 @@ describe('signal_rule_alert_type', () => {
payload.previousStartedAt = moment().subtract(10, 'm').toDate();
await alert.executor(payload);
expect(logger.warn).toHaveBeenCalledTimes(0);
expect(ruleStatusService.error).toHaveBeenCalledTimes(0);
expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenCalledTimes(2);
expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
newStatus: RuleExecutionStatus['going to run'],
})
);
expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
newStatus: RuleExecutionStatus.succeeded,
})
);
});
it('should call scheduleActions if signalsCount was greater than 0 and rule has actions defined', async () => {
@ -426,7 +451,11 @@ describe('signal_rule_alert_type', () => {
await alert.executor(payload);
expect(checkPrivileges).toHaveBeenCalledTimes(0);
expect(ruleStatusService.success).toHaveBeenCalled();
expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenLastCalledWith(
expect.objectContaining({
newStatus: RuleExecutionStatus.succeeded,
})
);
});
});
});
@ -450,7 +479,11 @@ describe('signal_rule_alert_type', () => {
expect(logger.error.mock.calls[0][0]).toContain(
'Bulk Indexing of signals failed: Error that bubbled up. name: "Detect Root/Admin Users" id: "04128c15-0d1b-4716-a4c5-46997ac7f3bd" rule id: "rule-1" signals index: ".siem-signals"'
);
expect(ruleStatusService.error).toHaveBeenCalled();
expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenLastCalledWith(
expect.objectContaining({
newStatus: RuleExecutionStatus.failed,
})
);
});
it('when error was thrown', async () => {
@ -458,10 +491,14 @@ describe('signal_rule_alert_type', () => {
await alert.executor(payload);
expect(logger.error).toHaveBeenCalled();
expect(logger.error.mock.calls[0][0]).toContain('An error occurred during rule execution');
expect(ruleStatusService.error).toHaveBeenCalled();
expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenLastCalledWith(
expect.objectContaining({
newStatus: RuleExecutionStatus.failed,
})
);
});
it('and call ruleStatusService with the default message', async () => {
it('and log failure with the default message', async () => {
(queryExecutor as jest.Mock).mockReturnValue(
elasticsearchClientMock.createErrorTransportRequestPromise(
new ResponseError(
@ -475,7 +512,11 @@ describe('signal_rule_alert_type', () => {
await alert.executor(payload);
expect(logger.error).toHaveBeenCalled();
expect(logger.error.mock.calls[0][0]).toContain('An error occurred during rule execution');
expect(ruleStatusService.error).toHaveBeenCalled();
expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenLastCalledWith(
expect.objectContaining({
newStatus: RuleExecutionStatus.failed,
})
);
});
});
});

View file

@ -45,7 +45,6 @@ import {
scheduleNotificationActions,
NotificationRuleTypeParams,
} from '../notifications/schedule_notification_actions';
import { ruleStatusServiceFactory } from './rule_status_service';
import { buildRuleMessageFactory } from './rule_messages';
import { getNotificationResultsLink } from '../notifications/utils';
import { TelemetryEventsSender } from '../../telemetry/sender';
@ -72,6 +71,7 @@ import { ExperimentalFeatures } from '../../../../common/experimental_features';
import { injectReferences, extractReferences } from './saved_object_references';
import { RuleExecutionLogClient } from '../rule_execution_log/rule_execution_log_client';
import { IRuleDataPluginService } from '../rule_execution_log/types';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
export const signalRulesAlertType = ({
logger,
@ -137,11 +137,6 @@ export const signalRulesAlertType = ({
ruleDataService,
savedObjectsClient: services.savedObjectsClient,
});
const ruleStatusService = await ruleStatusServiceFactory({
spaceId,
alertId,
ruleStatusClient,
});
const savedObject = await services.savedObjectsClient.get<AlertAttributes>('alert', alertId);
const {
@ -160,7 +155,11 @@ export const signalRulesAlertType = ({
logger.debug(buildRuleMessage('[+] Starting Signal Rule execution'));
logger.debug(buildRuleMessage(`interval: ${interval}`));
let wroteWarningStatus = false;
await ruleStatusService.goingToRun();
await ruleStatusClient.logStatusChange({
ruleId: alertId,
newStatus: RuleExecutionStatus['going to run'],
spaceId,
});
// check if rule has permissions to access given index pattern
// move this collection of lines into a function in utils
@ -190,22 +189,33 @@ export const signalRulesAlertType = ({
() =>
tryCatch(
() =>
hasReadIndexPrivileges(privileges, logger, buildRuleMessage, ruleStatusService),
hasReadIndexPrivileges({
spaceId,
ruleId: alertId,
privileges,
logger,
buildRuleMessage,
ruleStatusClient,
}),
toError
),
chain((wroteStatus) =>
tryCatch(
() =>
hasTimestampFields(
wroteStatus,
hasTimestampOverride ? (timestampOverride as string) : '@timestamp',
name,
timestampFieldCaps,
hasTimestampFields({
spaceId,
ruleId: alertId,
wroteStatus: wroteStatus as boolean,
timestampField: hasTimestampOverride
? (timestampOverride as string)
: '@timestamp',
ruleName: name,
timestampFieldCapsResponse: timestampFieldCaps,
inputIndices,
ruleStatusService,
ruleStatusClient,
logger,
buildRuleMessage
),
buildRuleMessage,
}),
toError
)
),
@ -232,7 +242,13 @@ export const signalRulesAlertType = ({
);
logger.warn(gapMessage);
hasError = true;
await ruleStatusService.error(gapMessage, { gap: gapString });
await ruleStatusClient.logStatusChange({
spaceId,
ruleId: alertId,
newStatus: RuleExecutionStatus.failed,
message: gapMessage,
metrics: { gap: gapString },
});
}
try {
const { listClient, exceptionsClient } = getListsClient({
@ -359,7 +375,12 @@ export const signalRulesAlertType = ({
}
if (result.warningMessages.length) {
const warningMessage = buildRuleMessage(result.warningMessages.join());
await ruleStatusService.partialFailure(warningMessage);
await ruleStatusClient.logStatusChange({
spaceId,
ruleId: alertId,
newStatus: RuleExecutionStatus['partial failure'],
message: warningMessage,
});
}
if (result.success) {
@ -403,10 +424,16 @@ export const signalRulesAlertType = ({
)
);
if (!hasError && !wroteWarningStatus && !result.warning) {
await ruleStatusService.success('succeeded', {
bulkCreateTimeDurations: result.bulkCreateTimes,
searchAfterTimeDurations: result.searchAfterTimes,
lastLookBackDate: result.lastLookBackDate?.toISOString(),
await ruleStatusClient.logStatusChange({
spaceId,
ruleId: alertId,
newStatus: RuleExecutionStatus.succeeded,
message: 'succeeded',
metrics: {
bulkCreateTimeDurations: result.bulkCreateTimes,
searchAfterTimeDurations: result.searchAfterTimes,
lastLookBackDate: result.lastLookBackDate?.toISOString(),
},
});
}
@ -426,10 +453,16 @@ export const signalRulesAlertType = ({
result.errors.join()
);
logger.error(errorMessage);
await ruleStatusService.error(errorMessage, {
bulkCreateTimeDurations: result.bulkCreateTimes,
searchAfterTimeDurations: result.searchAfterTimes,
lastLookBackDate: result.lastLookBackDate?.toISOString(),
await ruleStatusClient.logStatusChange({
spaceId,
ruleId: alertId,
newStatus: RuleExecutionStatus.failed,
message: errorMessage,
metrics: {
bulkCreateTimeDurations: result.bulkCreateTimes,
searchAfterTimeDurations: result.searchAfterTimes,
lastLookBackDate: result.lastLookBackDate?.toISOString(),
},
});
}
} catch (error) {
@ -440,10 +473,16 @@ export const signalRulesAlertType = ({
);
logger.error(message);
await ruleStatusService.error(message, {
bulkCreateTimeDurations: result.bulkCreateTimes,
searchAfterTimeDurations: result.searchAfterTimes,
lastLookBackDate: result.lastLookBackDate?.toISOString(),
await ruleStatusClient.logStatusChange({
spaceId,
ruleId: alertId,
newStatus: RuleExecutionStatus.failed,
message,
metrics: {
bulkCreateTimeDurations: result.bulkCreateTimes,
searchAfterTimeDurations: result.searchAfterTimes,
lastLookBackDate: result.lastLookBackDate?.toISOString(),
},
});
}
},

View file

@ -58,6 +58,7 @@ import {
sampleDocNoSortId,
} from './__mocks__/es_results';
import { ShardError } from '../../types';
import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client';
const buildRuleMessage = buildRuleMessageFactory({
id: 'fake id',
@ -66,13 +67,7 @@ const buildRuleMessage = buildRuleMessageFactory({
name: 'fake name',
});
const ruleStatusServiceMock = {
success: jest.fn(),
find: jest.fn(),
goingToRun: jest.fn(),
error: jest.fn(),
partialFailure: jest.fn(),
};
const ruleStatusClient = ruleExecutionLogClientMock.create();
describe('utils', () => {
const anchor = '2020-01-01T06:06:06.666Z';
@ -785,17 +780,19 @@ describe('utils', () => {
},
};
mockLogger.error.mockClear();
const res = await hasTimestampFields(
false,
const res = await hasTimestampFields({
wroteStatus: false,
timestampField,
'myfakerulename',
ruleName: 'myfakerulename',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
timestampFieldCapsResponse as ApiResponse<Record<string, any>>,
['myfa*'],
ruleStatusServiceMock,
mockLogger,
buildRuleMessage
);
timestampFieldCapsResponse: timestampFieldCapsResponse as ApiResponse<Record<string, any>>,
inputIndices: ['myfa*'],
ruleStatusClient,
ruleId: 'ruleId',
spaceId: 'default',
logger: mockLogger,
buildRuleMessage,
});
expect(mockLogger.error).toHaveBeenCalledWith(
'The following indices are missing the timestamp override field "event.ingested": ["myfakeindex-1","myfakeindex-2"] name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"'
);
@ -826,17 +823,19 @@ describe('utils', () => {
},
};
mockLogger.error.mockClear();
const res = await hasTimestampFields(
false,
const res = await hasTimestampFields({
wroteStatus: false,
timestampField,
'myfakerulename',
ruleName: 'myfakerulename',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
timestampFieldCapsResponse as ApiResponse<Record<string, any>>,
['myfa*'],
ruleStatusServiceMock,
mockLogger,
buildRuleMessage
);
timestampFieldCapsResponse: timestampFieldCapsResponse as ApiResponse<Record<string, any>>,
inputIndices: ['myfa*'],
ruleStatusClient,
ruleId: 'ruleId',
spaceId: 'default',
logger: mockLogger,
buildRuleMessage,
});
expect(mockLogger.error).toHaveBeenCalledWith(
'The following indices are missing the timestamp field "@timestamp": ["myfakeindex-1","myfakeindex-2"] name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"'
);
@ -853,17 +852,19 @@ describe('utils', () => {
},
};
mockLogger.error.mockClear();
const res = await hasTimestampFields(
false,
const res = await hasTimestampFields({
wroteStatus: false,
timestampField,
'Endpoint Security',
ruleName: 'Endpoint Security',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
timestampFieldCapsResponse as ApiResponse<Record<string, any>>,
['logs-endpoint.alerts-*'],
ruleStatusServiceMock,
mockLogger,
buildRuleMessage
);
timestampFieldCapsResponse: timestampFieldCapsResponse as ApiResponse<Record<string, any>>,
inputIndices: ['logs-endpoint.alerts-*'],
ruleStatusClient,
ruleId: 'ruleId',
spaceId: 'default',
logger: mockLogger,
buildRuleMessage,
});
expect(mockLogger.error).toHaveBeenCalledWith(
'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent. name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"'
);
@ -880,17 +881,19 @@ describe('utils', () => {
},
};
mockLogger.error.mockClear();
const res = await hasTimestampFields(
false,
const res = await hasTimestampFields({
wroteStatus: false,
timestampField,
'NOT Endpoint Security',
ruleName: 'NOT Endpoint Security',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
timestampFieldCapsResponse as ApiResponse<Record<string, any>>,
['logs-endpoint.alerts-*'],
ruleStatusServiceMock,
mockLogger,
buildRuleMessage
);
timestampFieldCapsResponse: timestampFieldCapsResponse as ApiResponse<Record<string, any>>,
inputIndices: ['logs-endpoint.alerts-*'],
ruleStatusClient,
ruleId: 'ruleId',
spaceId: 'default',
logger: mockLogger,
buildRuleMessage,
});
expect(mockLogger.error).toHaveBeenCalledWith(
'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"'
);

View file

@ -22,6 +22,7 @@ import { ElasticsearchClient } from '@kbn/securitysolution-es-utils';
import {
TimestampOverrideOrUndefined,
Privilege,
RuleExecutionStatus,
} from '../../../../common/detection_engine/schemas/common/schemas';
import { Logger, SavedObjectsClientContract } from '../../../../../../../src/core/server';
import {
@ -46,7 +47,6 @@ import {
} from './types';
import { BuildRuleMessage } from './rule_messages';
import { ShardError } from '../../types';
import { RuleStatusService } from './rule_status_service';
import {
EqlRuleParams,
MachineLearningRuleParams,
@ -58,6 +58,7 @@ import {
} from '../schemas/rule_schemas';
import { WrappedRACAlert } from '../rule_types/types';
import { SearchTypes } from '../../../../common/detection_engine/types';
import { IRuleExecutionLogClient } from '../rule_execution_log/types';
interface SortExceptionsReturn {
exceptionsWithValueLists: ExceptionListItemSchema[];
@ -81,12 +82,16 @@ export const shorthandMap = {
},
};
export const hasReadIndexPrivileges = async (
privileges: Privilege,
logger: Logger,
buildRuleMessage: BuildRuleMessage,
ruleStatusService: RuleStatusService
): Promise<boolean> => {
export const hasReadIndexPrivileges = async (args: {
privileges: Privilege;
logger: Logger;
buildRuleMessage: BuildRuleMessage;
ruleStatusClient: IRuleExecutionLogClient;
ruleId: string;
spaceId: string;
}): Promise<boolean> => {
const { privileges, logger, buildRuleMessage, ruleStatusClient, ruleId, spaceId } = args;
const indexNames = Object.keys(privileges.index);
const [indexesWithReadPrivileges, indexesWithNoReadPrivileges] = partition(
indexNames,
@ -100,7 +105,12 @@ export const hasReadIndexPrivileges = async (
indexesWithNoReadPrivileges
)}`;
logger.error(buildRuleMessage(errorString));
await ruleStatusService.partialFailure(errorString);
await ruleStatusClient.logStatusChange({
message: errorString,
ruleId,
spaceId,
newStatus: RuleExecutionStatus['partial failure'],
});
return true;
} else if (
indexesWithReadPrivileges.length === 0 &&
@ -112,25 +122,45 @@ export const hasReadIndexPrivileges = async (
indexesWithNoReadPrivileges
)}`;
logger.error(buildRuleMessage(errorString));
await ruleStatusService.partialFailure(errorString);
await ruleStatusClient.logStatusChange({
message: errorString,
ruleId,
spaceId,
newStatus: RuleExecutionStatus['partial failure'],
});
return true;
}
return false;
};
export const hasTimestampFields = async (
wroteStatus: boolean,
timestampField: string,
ruleName: string,
export const hasTimestampFields = async (args: {
wroteStatus: boolean;
timestampField: string;
ruleName: string;
// any is derived from here
// node_modules/@elastic/elasticsearch/api/kibana.d.ts
// eslint-disable-next-line @typescript-eslint/no-explicit-any
timestampFieldCapsResponse: ApiResponse<Record<string, any>, Context>,
inputIndices: string[],
ruleStatusService: RuleStatusService,
logger: Logger,
buildRuleMessage: BuildRuleMessage
): Promise<boolean> => {
timestampFieldCapsResponse: ApiResponse<Record<string, any>, Context>;
inputIndices: string[];
ruleStatusClient: IRuleExecutionLogClient;
ruleId: string;
spaceId: string;
logger: Logger;
buildRuleMessage: BuildRuleMessage;
}): Promise<boolean> => {
const {
wroteStatus,
timestampField,
ruleName,
timestampFieldCapsResponse,
inputIndices,
ruleStatusClient,
ruleId,
spaceId,
logger,
buildRuleMessage,
} = args;
if (!wroteStatus && isEmpty(timestampFieldCapsResponse.body.indices)) {
const errorString = `This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ${JSON.stringify(
inputIndices
@ -140,7 +170,12 @@ export const hasTimestampFields = async (
: ''
}`;
logger.error(buildRuleMessage(errorString.trimEnd()));
await ruleStatusService.partialFailure(errorString.trimEnd());
await ruleStatusClient.logStatusChange({
message: errorString.trimEnd(),
ruleId,
spaceId,
newStatus: RuleExecutionStatus['partial failure'],
});
return true;
} else if (
!wroteStatus &&
@ -161,7 +196,12 @@ export const hasTimestampFields = async (
: timestampFieldCapsResponse.body.fields[timestampField]?.unmapped?.indices
)}`;
logger.error(buildRuleMessage(errorString));
await ruleStatusService.partialFailure(errorString);
await ruleStatusClient.logStatusChange({
message: errorString,
ruleId,
spaceId,
newStatus: RuleExecutionStatus['partial failure'],
});
return true;
}
return wroteStatus;