Cleanup after ExecLog integration (#107695)
This commit is contained in:
parent
dc9da8ebe0
commit
1f73c0fcfa
|
@ -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>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
export class InvariantError extends Error {
|
||||
name = 'Invariant violation';
|
||||
name = 'InvariantError';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
|
@ -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(
|
|
@ -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';
|
|
@ -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 = {
|
|
@ -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: (
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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> = {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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];
|
||||
};
|
|
@ -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(),
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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)));
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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"'
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue