Implement RuleExecutionLog (#103463)

This commit is contained in:
Dmitry Shevchenko 2021-08-03 15:25:26 +02:00 committed by GitHub
parent 4b4525ab05
commit fddd9d7992
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 1384 additions and 396 deletions

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { left } from 'fp-ts/lib/Either';
import { enumeration } from '.';
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
describe('enumeration', () => {
enum TestEnum {
'test' = 'test',
}
it('should validate a string from the enum', () => {
const input = TestEnum.test;
const codec = enumeration('TestEnum', TestEnum);
const decoded = codec.decode(input);
const message = foldLeftRight(decoded);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(input);
});
it('should NOT validate a random string', () => {
const input = 'some string';
const codec = enumeration('TestEnum', TestEnum);
const decoded = codec.decode(input);
const message = foldLeftRight(decoded);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "some string" supplied to "TestEnum"',
]);
expect(message.schema).toEqual({});
});
});

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
export function enumeration<EnumType extends string>(
name: string,
originalEnum: Record<string, EnumType>
): t.Type<EnumType, EnumType, unknown> {
const isEnumValue = (input: unknown): input is EnumType =>
Object.values<unknown>(originalEnum).includes(input);
return new t.Type<EnumType>(
name,
isEnumValue,
(input, context) => (isEnumValue(input) ? t.success(input) : t.failure(input, context)),
t.identity
);
}

View file

@ -15,15 +15,16 @@ export * from './default_string_boolean_false';
export * from './default_uuid'; export * from './default_uuid';
export * from './default_version_number'; export * from './default_version_number';
export * from './empty_string_array'; export * from './empty_string_array';
export * from './enumeration';
export * from './iso_date_string'; export * from './iso_date_string';
export * from './non_empty_array'; export * from './non_empty_array';
export * from './non_empty_or_nullable_string_array'; export * from './non_empty_or_nullable_string_array';
export * from './non_empty_string';
export * from './non_empty_string_array'; export * from './non_empty_string_array';
export * from './operator'; export * from './non_empty_string';
export * from './only_false_allowed'; export * from './only_false_allowed';
export * from './positive_integer'; export * from './operator';
export * from './positive_integer_greater_than_zero'; export * from './positive_integer_greater_than_zero';
export * from './positive_integer';
export * from './string_to_positive_number'; export * from './string_to_positive_number';
export * from './uuid'; export * from './uuid';
export * from './version'; export * from './version';

View file

@ -18,13 +18,14 @@ export {
createLifecycleRuleTypeFactory, createLifecycleRuleTypeFactory,
LifecycleAlertService, LifecycleAlertService,
} from './utils/create_lifecycle_rule_type_factory'; } from './utils/create_lifecycle_rule_type_factory';
export { RuleDataPluginService } from './rule_data_plugin_service';
export { export {
LifecycleRuleExecutor, LifecycleRuleExecutor,
LifecycleAlertServices, LifecycleAlertServices,
createLifecycleExecutor, createLifecycleExecutor,
} from './utils/create_lifecycle_executor'; } from './utils/create_lifecycle_executor';
export { createPersistenceRuleTypeFactory } from './utils/create_persistence_rule_type_factory'; export { createPersistenceRuleTypeFactory } from './utils/create_persistence_rule_type_factory';
export type { AlertTypeWithExecutor } from './types'; export { AlertTypeWithExecutor } from './types';
export const plugin = (initContext: PluginInitializerContext) => export const plugin = (initContext: PluginInitializerContext) =>
new RuleRegistryPlugin(initContext); new RuleRegistryPlugin(initContext);

View file

@ -147,6 +147,12 @@ export class RuleDataPluginService {
return; return;
} catch (err) { } catch (err) {
if (err.meta?.body?.error?.type !== 'illegal_argument_exception') { if (err.meta?.body?.error?.type !== 'illegal_argument_exception') {
/**
* We skip the rollover if we catch anything except for illegal_argument_exception - that's the error
* returned by ES when the mapping update contains a conflicting field definition (e.g., a field changes types).
* We expect to get that error for some mapping changes we might make, and in those cases,
* we want to continue to rollover the index. Other errors are unexpected.
*/
this.options.logger.error(`Failed to PUT mapping for alias ${alias}: ${err.message}`); this.options.logger.error(`Failed to PUT mapping for alias ${alias}: ${err.message}`);
return; return;
} }
@ -161,6 +167,10 @@ export class RuleDataPluginService {
new_index: newIndexName, new_index: newIndexName,
}); });
} catch (e) { } catch (e) {
/**
* If we catch resource_already_exists_exception, that means that the index has been
* rolled over already nothing to do for us in this case.
*/
if (e?.meta?.body?.error?.type !== 'resource_already_exists_exception') { if (e?.meta?.body?.error?.type !== 'resource_already_exists_exception') {
this.options.logger.error(`Failed to rollover index for alias ${alias}: ${e.message}`); this.options.logger.error(`Failed to rollover index for alias ${alias}: ${e.message}`);
} }

View file

@ -55,6 +55,7 @@ export const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000; // ms
export const DEFAULT_RULE_REFRESH_IDLE_VALUE = 2700000; // ms export const DEFAULT_RULE_REFRESH_IDLE_VALUE = 2700000; // ms
export const DEFAULT_RULE_NOTIFICATION_QUERY_SIZE = 100; export const DEFAULT_RULE_NOTIFICATION_QUERY_SIZE = 100;
export const SAVED_OBJECTS_MANAGEMENT_FEATURE_ID = 'Saved Objects Management'; export const SAVED_OBJECTS_MANAGEMENT_FEATURE_ID = 'Saved Objects Management';
export const DEFAULT_SPACE_ID = 'default';
// Document path where threat indicator fields are expected. Fields are used // Document path where threat indicator fields are expected. Fields are used
// to enrich signals, and are copied to threat.indicator. // to enrich signals, and are copied to threat.indicator.

View file

@ -7,15 +7,15 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
import * as t from 'io-ts';
import { import {
UUID, enumeration,
NonEmptyString,
IsoDateString, IsoDateString,
PositiveIntegerGreaterThanZero, NonEmptyString,
PositiveInteger, PositiveInteger,
PositiveIntegerGreaterThanZero,
UUID,
} from '@kbn/securitysolution-io-ts-types'; } from '@kbn/securitysolution-io-ts-types';
import * as t from 'io-ts';
export const author = t.array(t.string); export const author = t.array(t.string);
export type Author = t.TypeOf<typeof author>; export type Author = t.TypeOf<typeof author>;
@ -173,14 +173,18 @@ export type RuleNameOverrideOrUndefined = t.TypeOf<typeof ruleNameOverrideOrUnde
export const status = t.keyof({ open: null, closed: null, 'in-progress': null }); export const status = t.keyof({ open: null, closed: null, 'in-progress': null });
export type Status = t.TypeOf<typeof status>; export type Status = t.TypeOf<typeof status>;
export const job_status = t.keyof({ export enum RuleExecutionStatus {
succeeded: null, 'succeeded' = 'succeeded',
failed: null, 'failed' = 'failed',
'going to run': null, 'going to run' = 'going to run',
'partial failure': null, 'partial failure' = 'partial failure',
warning: null, /**
}); * @deprecated 'partial failure' status should be used instead
export type JobStatus = t.TypeOf<typeof job_status>; */
'warning' = 'warning',
}
export const ruleExecutionStatus = enumeration('RuleExecutionStatus', RuleExecutionStatus);
export const conflicts = t.keyof({ abort: null, proceed: null }); export const conflicts = t.keyof({ abort: null, proceed: null });
export type Conflicts = t.TypeOf<typeof conflicts>; export type Conflicts = t.TypeOf<typeof conflicts>;
@ -419,4 +423,4 @@ export enum BulkAction {
'duplicate' = 'duplicate', 'duplicate' = 'duplicate',
} }
export const bulkAction = t.keyof(BulkAction); export const bulkAction = enumeration('BulkAction', BulkAction);

View file

@ -62,7 +62,7 @@ import {
updated_by, updated_by,
created_at, created_at,
created_by, created_by,
job_status, ruleExecutionStatus,
status_date, status_date,
last_success_at, last_success_at,
last_success_message, last_success_message,
@ -405,7 +405,7 @@ const responseRequiredFields = {
created_by, created_by,
}; };
const responseOptionalFields = { const responseOptionalFields = {
status: job_status, status: ruleExecutionStatus,
status_date, status_date,
last_success_at, last_success_at,
last_success_message, last_success_message,

View file

@ -6,6 +6,7 @@
*/ */
import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../constants'; import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../constants';
import { RuleExecutionStatus } from '../common/schemas';
import { getListArrayMock } from '../types/lists.mock'; import { getListArrayMock } from '../types/lists.mock';
import { RulesSchema } from './rules_schema'; import { RulesSchema } from './rules_schema';
@ -60,7 +61,7 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchem
type: 'query', type: 'query',
threat: [], threat: [],
version: 1, version: 1,
status: 'succeeded', status: RuleExecutionStatus.succeeded,
status_date: '2020-02-22T16:47:50.047Z', status_date: '2020-02-22T16:47:50.047Z',
last_success_at: '2020-02-22T16:47:50.047Z', last_success_at: '2020-02-22T16:47:50.047Z',
last_success_message: 'succeeded', last_success_message: 'succeeded',

View file

@ -62,7 +62,7 @@ import {
timeline_id, timeline_id,
timeline_title, timeline_title,
threshold, threshold,
job_status, ruleExecutionStatus,
status_date, status_date,
last_success_at, last_success_at,
last_success_message, last_success_message,
@ -164,7 +164,7 @@ export const partialRulesSchema = t.partial({
license, license,
throttle, throttle,
rule_name_override, rule_name_override,
status: job_status, status: ruleExecutionStatus,
status_date, status_date,
timestamp_override, timestamp_override,
last_success_at, last_success_at,

View file

@ -16,7 +16,7 @@ import type {
import { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { Type } from '@kbn/securitysolution-io-ts-alerting-types';
import { hasLargeValueList } from '@kbn/securitysolution-list-utils'; import { hasLargeValueList } from '@kbn/securitysolution-list-utils';
import { JobStatus, Threshold, ThresholdNormalized } from './schemas/common/schemas'; import { RuleExecutionStatus, Threshold, ThresholdNormalized } from './schemas/common/schemas';
export const hasLargeValueItem = ( export const hasLargeValueItem = (
exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema> exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>
@ -64,5 +64,11 @@ export const normalizeThresholdObject = (threshold: Threshold): ThresholdNormali
export const normalizeMachineLearningJobIds = (value: string | string[]): string[] => export const normalizeMachineLearningJobIds = (value: string | string[]): string[] =>
Array.isArray(value) ? value : [value]; Array.isArray(value) ? value : [value];
export const getRuleStatusText = (value: JobStatus | null | undefined): JobStatus | null => export const getRuleStatusText = (
value === 'partial failure' ? 'warning' : value != null ? value : null; value: RuleExecutionStatus | null | undefined
): RuleExecutionStatus | null =>
value === RuleExecutionStatus['partial failure']
? RuleExecutionStatus.warning
: value != null
? value
: null;

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export class InvariantError extends Error {
name = 'Invariant violation';
}
/**
* Asserts that the provided condition is always true
* and throws an invariant violation error otherwise
*
* @param condition Condition to assert
* @param message Error message to throw if the condition is falsy
*/
export function invariant(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new InvariantError(message);
}
}

View file

@ -5,9 +5,9 @@
* 2.0. * 2.0.
*/ */
import { RuleStatusType } from '../../../containers/detection_engine/rules'; import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
export const getStatusColor = (status: RuleStatusType | string | null) => export const getStatusColor = (status: RuleExecutionStatus | string | null) =>
status == null status == null
? 'subdued' ? 'subdued'
: status === 'succeeded' : status === 'succeeded'

View file

@ -8,16 +8,16 @@
import { EuiFlexItem, EuiHealth, EuiText } from '@elastic/eui'; import { EuiFlexItem, EuiHealth, EuiText } from '@elastic/eui';
import React, { memo } from 'react'; import React, { memo } from 'react';
import { RuleStatusType } from '../../../containers/detection_engine/rules';
import { FormattedDate } from '../../../../common/components/formatted_date'; import { FormattedDate } from '../../../../common/components/formatted_date';
import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { getEmptyTagValue } from '../../../../common/components/empty_value';
import { getStatusColor } from './helpers'; import { getStatusColor } from './helpers';
import * as i18n from './translations'; import * as i18n from './translations';
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
interface RuleStatusProps { interface RuleStatusProps {
children: React.ReactNode | null | undefined; children: React.ReactNode | null | undefined;
statusDate: string | null | undefined; statusDate: string | null | undefined;
status: RuleStatusType | null | undefined; status: RuleExecutionStatus | null | undefined;
} }
const RuleStatusComponent: React.FC<RuleStatusProps> = ({ children, statusDate, status }) => { const RuleStatusComponent: React.FC<RuleStatusProps> = ({ children, statusDate, status }) => {

View file

@ -20,6 +20,7 @@ import {
import { savedRuleMock, rulesMock } from '../mock'; import { savedRuleMock, rulesMock } from '../mock';
import { getRulesSchemaMock } from '../../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { getRulesSchemaMock } from '../../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
import { RulesSchema } from '../../../../../../common/detection_engine/schemas/response'; import { RulesSchema } from '../../../../../../common/detection_engine/schemas/response';
import { RuleExecutionStatus } from '../../../../../../common/detection_engine/schemas/common/schemas';
export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise<RulesSchema> => export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise<RulesSchema> =>
Promise.resolve(getRulesSchemaMock()); Promise.resolve(getRulesSchemaMock());
@ -60,7 +61,7 @@ export const getRuleStatusById = async ({
current_status: { current_status: {
alert_id: 'alertId', alert_id: 'alertId',
status_date: 'mm/dd/yyyyTHH:MM:sssz', status_date: 'mm/dd/yyyyTHH:MM:sssz',
status: 'succeeded', status: RuleExecutionStatus.succeeded,
last_failure_at: null, last_failure_at: null,
last_success_at: 'mm/dd/yyyyTHH:MM:sssz', last_success_at: 'mm/dd/yyyyTHH:MM:sssz',
last_failure_message: null, last_failure_message: null,
@ -86,7 +87,7 @@ export const getRulesStatusByIds = async ({
current_status: { current_status: {
alert_id: 'alertId', alert_id: 'alertId',
status_date: 'mm/dd/yyyyTHH:MM:sssz', status_date: 'mm/dd/yyyyTHH:MM:sssz',
status: 'succeeded', status: RuleExecutionStatus.succeeded,
last_failure_at: null, last_failure_at: null,
last_success_at: 'mm/dd/yyyyTHH:MM:sssz', last_success_at: 'mm/dd/yyyyTHH:MM:sssz',
last_failure_message: null, last_failure_message: null,

View file

@ -29,6 +29,8 @@ import {
timestamp_override, timestamp_override,
threshold, threshold,
BulkAction, BulkAction,
ruleExecutionStatus,
RuleExecutionStatus,
} from '../../../../../common/detection_engine/schemas/common/schemas'; } from '../../../../../common/detection_engine/schemas/common/schemas';
import { import {
CreateRulesSchema, CreateRulesSchema,
@ -75,14 +77,6 @@ const MetaRule = t.intersection([
}), }),
]); ]);
const StatusTypes = t.union([
t.literal('succeeded'),
t.literal('failed'),
t.literal('going to run'),
t.literal('partial failure'),
t.literal('warning'),
]);
// TODO: make a ticket // TODO: make a ticket
export const RuleSchema = t.intersection([ export const RuleSchema = t.intersection([
t.type({ t.type({
@ -130,7 +124,7 @@ export const RuleSchema = t.intersection([
query: t.string, query: t.string,
rule_name_override, rule_name_override,
saved_id: t.string, saved_id: t.string,
status: StatusTypes, status: ruleExecutionStatus,
status_date: t.string, status_date: t.string,
threshold, threshold,
threat_query, threat_query,
@ -274,17 +268,10 @@ export interface RuleStatus {
current_status: RuleInfoStatus; current_status: RuleInfoStatus;
failures: RuleInfoStatus[]; failures: RuleInfoStatus[];
} }
export type RuleStatusType =
| 'failed'
| 'going to run'
| 'succeeded'
| 'partial failure'
| 'warning';
export interface RuleInfoStatus { export interface RuleInfoStatus {
alert_id: string; alert_id: string;
status_date: string; status_date: string;
status: RuleStatusType | null; status: RuleExecutionStatus | null;
last_failure_at: string | null; last_failure_at: string | null;
last_success_at: string | null; last_success_at: string | null;
last_failure_message: string | null; last_failure_message: string | null;

View file

@ -14,12 +14,14 @@ import {
import { rulesClientMock } from '../../../../../../alerting/server/mocks'; import { rulesClientMock } from '../../../../../../alerting/server/mocks';
import { licensingMock } from '../../../../../../licensing/server/mocks'; import { licensingMock } from '../../../../../../licensing/server/mocks';
import { siemMock } from '../../../../mocks'; import { siemMock } from '../../../../mocks';
import { RuleExecutionLogClient } from '../../rule_execution_log/__mocks__/rule_execution_log_client';
const createMockClients = () => ({ const createMockClients = () => ({
rulesClient: rulesClientMock.create(), rulesClient: rulesClientMock.create(),
licensing: { license: licensingMock.createLicenseMock() }, licensing: { license: licensingMock.createLicenseMock() },
clusterClient: elasticsearchServiceMock.createScopedClusterClient(), clusterClient: elasticsearchServiceMock.createScopedClusterClient(),
savedObjectsClient: savedObjectsClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(),
ruleExecutionLogClient: new RuleExecutionLogClient(),
appClient: siemMock.createClient(), appClient: siemMock.createClient(),
}); });
@ -57,7 +59,11 @@ const createRequestContextMock = (
savedObjects: { client: clients.savedObjectsClient }, savedObjects: { client: clients.savedObjectsClient },
}, },
licensing: clients.licensing, licensing: clients.licensing,
securitySolution: { getAppClient: jest.fn(() => clients.appClient) }, securitySolution: {
getAppClient: jest.fn(() => clients.appClient),
getExecutionLogClient: jest.fn(() => clients.ruleExecutionLogClient),
getSpaceId: jest.fn(() => 'default'),
},
} as unknown) as SecuritySolutionRequestHandlerContextMock; } as unknown) as SecuritySolutionRequestHandlerContextMock;
}; };

View file

@ -5,7 +5,7 @@
* 2.0. * 2.0.
*/ */
import { SavedObjectsFindResponse } from 'kibana/server'; import { SavedObjectsFindResponse, SavedObjectsFindResult } from 'kibana/server';
import { ActionResult } from '../../../../../../actions/server'; import { ActionResult } from '../../../../../../actions/server';
import { SignalSearchResponse } from '../../signals/types'; import { SignalSearchResponse } from '../../signals/types';
import { import {
@ -24,6 +24,7 @@ import {
RuleAlertType, RuleAlertType,
IRuleSavedAttributesSavedObjectAttributes, IRuleSavedAttributesSavedObjectAttributes,
HapiReadableStream, HapiReadableStream,
IRuleStatusSOAttributes,
} from '../../rules/types'; } from '../../rules/types';
import { requestMock } from './request'; import { requestMock } from './request';
import { RuleNotificationAlertType } from '../../notifications/types'; import { RuleNotificationAlertType } from '../../notifications/types';
@ -37,6 +38,8 @@ import { RuleParams } from '../../schemas/rule_schemas';
import { Alert } from '../../../../../../alerting/common'; import { Alert } from '../../../../../../alerting/common';
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
import { getPerformBulkActionSchemaMock } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock'; import { getPerformBulkActionSchemaMock } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock';
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
import { FindBulkExecutionLogResponse } from '../../rule_execution_log/types';
export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({
signal_ids: ['somefakeid1', 'somefakeid2'], signal_ids: ['somefakeid1', 'somefakeid2'],
@ -442,25 +445,23 @@ export const getMockPrivilegesResult = () => ({
application: {}, application: {},
}); });
export const getFindResultStatusEmpty = (): SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> => ({ export const getEmptySavedObjectsResponse = (): SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> => ({
page: 1, page: 1,
per_page: 1, per_page: 1,
total: 0, total: 0,
saved_objects: [], saved_objects: [],
}); });
export const getFindResultStatus = (): SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> => ({ export const getRuleExecutionStatuses = (): Array<
page: 1, SavedObjectsFindResult<IRuleStatusSOAttributes>
per_page: 6, > => [
total: 2,
saved_objects: [
{ {
type: 'my-type', type: 'my-type',
id: 'e0b86950-4e9f-11ea-bdbd-07b56aa159b3', id: 'e0b86950-4e9f-11ea-bdbd-07b56aa159b3',
attributes: { attributes: {
alertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bc', alertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bc',
statusDate: '2020-02-18T15:26:49.783Z', statusDate: '2020-02-18T15:26:49.783Z',
status: 'succeeded', status: RuleExecutionStatus.succeeded,
lastFailureAt: undefined, lastFailureAt: undefined,
lastSuccessAt: '2020-02-18T15:26:49.783Z', lastSuccessAt: '2020-02-18T15:26:49.783Z',
lastFailureMessage: undefined, lastFailureMessage: undefined,
@ -481,7 +482,7 @@ export const getFindResultStatus = (): SavedObjectsFindResponse<IRuleSavedAttrib
attributes: { attributes: {
alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08',
statusDate: '2020-02-18T15:15:58.806Z', statusDate: '2020-02-18T15:15:58.806Z',
status: 'failed', status: RuleExecutionStatus.failed,
lastFailureAt: '2020-02-18T15:15:58.806Z', lastFailureAt: '2020-02-18T15:15:58.806Z',
lastSuccessAt: '2020-02-13T20:31:59.855Z', lastSuccessAt: '2020-02-13T20:31:59.855Z',
lastFailureMessage: lastFailureMessage:
@ -497,28 +498,14 @@ export const getFindResultStatus = (): SavedObjectsFindResponse<IRuleSavedAttrib
updated_at: '2020-02-18T15:15:58.860Z', updated_at: '2020-02-18T15:15:58.860Z',
version: 'WzMyLDFd', version: 'WzMyLDFd',
}, },
], ];
});
export const getFindBulkResultStatus = (): SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> => ({ export const getFindBulkResultStatus = (): FindBulkExecutionLogResponse => ({
page: 1, '04128c15-0d1b-4716-a4c5-46997ac7f3bd': [
per_page: 6,
total: 2,
saved_objects: [],
aggregations: {
alertIds: {
buckets: [
{ {
key: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
most_recent_statuses: {
hits: {
hits: [
{
_source: {
'siem-detection-engine-rule-status': {
alertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', alertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
statusDate: '2020-02-18T15:26:49.783Z', statusDate: '2020-02-18T15:26:49.783Z',
status: 'succeeded', status: RuleExecutionStatus.succeeded,
lastFailureAt: undefined, lastFailureAt: undefined,
lastSuccessAt: '2020-02-18T15:26:49.783Z', lastSuccessAt: '2020-02-18T15:26:49.783Z',
lastFailureMessage: undefined, lastFailureMessage: undefined,
@ -528,23 +515,12 @@ export const getFindBulkResultStatus = (): SavedObjectsFindResponse<IRuleSavedAt
searchAfterTimeDurations: ['200.00'], searchAfterTimeDurations: ['200.00'],
bulkCreateTimeDurations: ['800.43'], bulkCreateTimeDurations: ['800.43'],
}, },
},
},
], ],
}, '1ea5a820-4da1-4e82-92a1-2b43a7bece08': [
},
},
{ {
key: '1ea5a820-4da1-4e82-92a1-2b43a7bece08',
most_recent_statuses: {
hits: {
hits: [
{
_source: {
'siem-detection-engine-rule-status': {
alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08',
statusDate: '2020-02-18T15:15:58.806Z', statusDate: '2020-02-18T15:15:58.806Z',
status: 'failed', status: RuleExecutionStatus.failed,
lastFailureAt: '2020-02-18T15:15:58.806Z', lastFailureAt: '2020-02-18T15:15:58.806Z',
lastSuccessAt: '2020-02-13T20:31:59.855Z', lastSuccessAt: '2020-02-13T20:31:59.855Z',
lastFailureMessage: lastFailureMessage:
@ -555,15 +531,7 @@ export const getFindBulkResultStatus = (): SavedObjectsFindResponse<IRuleSavedAt
searchAfterTimeDurations: ['200.00'], searchAfterTimeDurations: ['200.00'],
bulkCreateTimeDurations: ['800.43'], bulkCreateTimeDurations: ['800.43'],
}, },
},
},
], ],
},
},
},
],
},
},
}); });
export const getEmptySignalsResponse = (): SignalSearchResponse => ({ export const getEmptySignalsResponse = (): SignalSearchResponse => ({

View file

@ -116,6 +116,7 @@ export const createPrepackagedRules = async (
const exceptionsListClient = const exceptionsListClient =
context.lists != null ? context.lists.getExceptionListClient() : exceptionsClient; context.lists != null ? context.lists.getExceptionListClient() : exceptionsClient;
const ruleAssetsClient = ruleAssetSavedObjectsClientFactory(savedObjectsClient); const ruleAssetsClient = ruleAssetSavedObjectsClientFactory(savedObjectsClient);
const ruleStatusClient = context.securitySolution.getExecutionLogClient();
if (!siemClient || !rulesClient) { if (!siemClient || !rulesClient) {
throw new PrepackagedRulesError('', 404); throw new PrepackagedRulesError('', 404);
} }
@ -154,7 +155,13 @@ export const createPrepackagedRules = async (
timeline, timeline,
importTimelineResultSchema importTimelineResultSchema
); );
await updatePrepackagedRules(rulesClient, savedObjectsClient, rulesToUpdate, signalsIndex); await updatePrepackagedRules(
rulesClient,
context.securitySolution.getSpaceId(),
ruleStatusClient,
rulesToUpdate,
signalsIndex
);
const prepackagedRulesOutput: PrePackagedRulesAndTimelinesSchema = { const prepackagedRulesOutput: PrePackagedRulesAndTimelinesSchema = {
rules_installed: rulesToInstall.length, rules_installed: rulesToInstall.length,

View file

@ -10,7 +10,7 @@ import {
getEmptyFindResult, getEmptyFindResult,
getAlertMock, getAlertMock,
getCreateRequest, getCreateRequest,
getFindResultStatus, getRuleExecutionStatuses,
getFindResultWithSingleHit, getFindResultWithSingleHit,
createMlRuleRequest, createMlRuleRequest,
} from '../__mocks__/request_responses'; } from '../__mocks__/request_responses';
@ -38,7 +38,7 @@ describe('create_rules', () => {
clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); // no current rules clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); // no current rules
clients.rulesClient.create.mockResolvedValue(getAlertMock(getQueryRuleParams())); // creation succeeds clients.rulesClient.create.mockResolvedValue(getAlertMock(getQueryRuleParams())); // creation succeeds
clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); // needed to transform clients.ruleExecutionLogClient.find.mockResolvedValue(getRuleExecutionStatuses()); // needed to transform: ;
context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue( context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue(
elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } }) elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } })

View file

@ -17,7 +17,6 @@ import { readRules } from '../../rules/read_rules';
import { buildSiemResponse } from '../utils'; import { buildSiemResponse } from '../utils';
import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { updateRulesNotifications } from '../../rules/update_rules_notifications';
import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
import { createRulesSchema } from '../../../../../common/detection_engine/schemas/request'; import { createRulesSchema } from '../../../../../common/detection_engine/schemas/request';
import { newTransformValidate } from './validate'; import { newTransformValidate } from './validate';
import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents'; import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents';
@ -106,18 +105,12 @@ export const createRulesRoute = (
name: createdRule.name, name: createdRule.name,
}); });
const ruleStatuses = await ruleStatusSavedObjectsClientFactory(savedObjectsClient).find({ const ruleStatuses = await context.securitySolution.getExecutionLogClient().find({
perPage: 1, logsCount: 1,
sortField: 'statusDate', ruleId: createdRule.id,
sortOrder: 'desc', spaceId: context.securitySolution.getSpaceId(),
search: `${createdRule.id}`,
searchFields: ['alertId'],
}); });
const [validated, errors] = newTransformValidate( const [validated, errors] = newTransformValidate(createdRule, ruleActions, ruleStatuses[0]);
createdRule,
ruleActions,
ruleStatuses.saved_objects[0]
);
if (errors != null) { if (errors != null) {
return siemResponse.error({ statusCode: 500, body: errors }); return siemResponse.error({ statusCode: 500, body: errors });
} else { } else {

View file

@ -13,8 +13,7 @@ import {
getDeleteBulkRequestById, getDeleteBulkRequestById,
getDeleteAsPostBulkRequest, getDeleteAsPostBulkRequest,
getDeleteAsPostBulkRequestById, getDeleteAsPostBulkRequestById,
getFindResultStatusEmpty, getEmptySavedObjectsResponse,
getFindResultStatus,
} from '../__mocks__/request_responses'; } from '../__mocks__/request_responses';
import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { requestContextMock, serverMock, requestMock } from '../__mocks__';
import { deleteRulesBulkRoute } from './delete_rules_bulk_route'; import { deleteRulesBulkRoute } from './delete_rules_bulk_route';
@ -29,7 +28,7 @@ describe('delete_rules', () => {
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists
clients.rulesClient.delete.mockResolvedValue({}); // successful deletion clients.rulesClient.delete.mockResolvedValue({}); // successful deletion
clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatusEmpty()); // rule status request clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); // rule status request
deleteRulesBulkRoute(server.router); deleteRulesBulkRoute(server.router);
}); });
@ -41,7 +40,6 @@ describe('delete_rules', () => {
}); });
test('resturns 200 when deleting a single rule and related rule status', async () => { test('resturns 200 when deleting a single rule and related rule status', async () => {
clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
const response = await server.inject(getDeleteBulkRequest(), context); const response = await server.inject(getDeleteBulkRequest(), context);
expect(response.status).toEqual(200); expect(response.status).toEqual(200);
}); });

View file

@ -23,7 +23,6 @@ import { getIdBulkError } from './utils';
import { transformValidateBulkError } from './validate'; import { transformValidateBulkError } from './validate';
import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '../utils'; import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '../utils';
import { deleteRules } from '../../rules/delete_rules'; import { deleteRules } from '../../rules/delete_rules';
import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
import { readRules } from '../../rules/read_rules'; import { readRules } from '../../rules/read_rules';
type Config = RouteConfig<unknown, unknown, QueryRulesBulkSchemaDecoded, 'delete' | 'post'>; type Config = RouteConfig<unknown, unknown, QueryRulesBulkSchemaDecoded, 'delete' | 'post'>;
@ -57,7 +56,7 @@ export const deleteRulesBulkRoute = (router: SecuritySolutionPluginRouter) => {
return siemResponse.error({ statusCode: 404 }); return siemResponse.error({ statusCode: 404 });
} }
const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const ruleStatusClient = context.securitySolution.getExecutionLogClient();
const rules = await Promise.all( const rules = await Promise.all(
request.body.map(async (payloadRule) => { request.body.map(async (payloadRule) => {
@ -79,9 +78,9 @@ export const deleteRulesBulkRoute = (router: SecuritySolutionPluginRouter) => {
} }
const ruleStatuses = await ruleStatusClient.find({ const ruleStatuses = await ruleStatusClient.find({
perPage: 6, logsCount: 6,
search: rule.id, ruleId: rule.id,
searchFields: ['alertId'], spaceId: context.securitySolution.getSpaceId(),
}); });
await deleteRules({ await deleteRules({
rulesClient, rulesClient,

View file

@ -12,7 +12,8 @@ import {
getDeleteRequest, getDeleteRequest,
getFindResultWithSingleHit, getFindResultWithSingleHit,
getDeleteRequestById, getDeleteRequestById,
getFindResultStatus, getRuleExecutionStatuses,
getEmptySavedObjectsResponse,
} from '../__mocks__/request_responses'; } from '../__mocks__/request_responses';
import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { requestContextMock, serverMock, requestMock } from '../__mocks__';
import { deleteRulesRoute } from './delete_rules_route'; import { deleteRulesRoute } from './delete_rules_route';
@ -27,7 +28,8 @@ describe('delete_rules', () => {
({ clients, context } = requestContextMock.createTools()); ({ clients, context } = requestContextMock.createTools());
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit());
clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse());
clients.ruleExecutionLogClient.find.mockResolvedValue(getRuleExecutionStatuses());
deleteRulesRoute(server.router); deleteRulesRoute(server.router);
}); });

View file

@ -19,7 +19,6 @@ import { deleteRules } from '../../rules/delete_rules';
import { getIdError, transform } from './utils'; import { getIdError, transform } from './utils';
import { buildSiemResponse } from '../utils'; import { buildSiemResponse } from '../utils';
import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
import { readRules } from '../../rules/read_rules'; import { readRules } from '../../rules/read_rules';
export const deleteRulesRoute = ( export const deleteRulesRoute = (
@ -55,7 +54,7 @@ export const deleteRulesRoute = (
return siemResponse.error({ statusCode: 404 }); return siemResponse.error({ statusCode: 404 });
} }
const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const ruleStatusClient = context.securitySolution.getExecutionLogClient();
const rule = await readRules({ rulesClient, id, ruleId }); const rule = await readRules({ rulesClient, id, ruleId });
if (!rule) { if (!rule) {
const error = getIdError({ id, ruleId }); const error = getIdError({ id, ruleId });
@ -66,9 +65,9 @@ export const deleteRulesRoute = (
} }
const ruleStatuses = await ruleStatusClient.find({ const ruleStatuses = await ruleStatusClient.find({
perPage: 6, logsCount: 6,
search: rule.id, ruleId: rule.id,
searchFields: ['alertId'], spaceId: context.securitySolution.getSpaceId(),
}); });
await deleteRules({ await deleteRules({
rulesClient, rulesClient,
@ -77,7 +76,7 @@ export const deleteRulesRoute = (
ruleStatuses, ruleStatuses,
id: rule.id, id: rule.id,
}); });
const transformed = transform(rule, undefined, ruleStatuses.saved_objects[0]); const transformed = transform(rule, undefined, ruleStatuses[0]);
if (transformed == null) { if (transformed == null) {
return siemResponse.error({ statusCode: 500, body: 'failed to transform alert' }); return siemResponse.error({ statusCode: 500, body: 'failed to transform alert' });
} else { } else {

View file

@ -6,15 +6,16 @@
*/ */
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
import { requestContextMock, requestMock, serverMock } from '../__mocks__';
import { import {
getAlertMock, getAlertMock,
getFindRequest,
getFindResultWithSingleHit,
getFindBulkResultStatus, getFindBulkResultStatus,
getFindRequest,
getEmptySavedObjectsResponse,
getFindResultWithSingleHit,
} from '../__mocks__/request_responses'; } from '../__mocks__/request_responses';
import { requestContextMock, serverMock, requestMock } from '../__mocks__';
import { findRulesRoute } from './find_rules_route'; import { findRulesRoute } from './find_rules_route';
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
jest.mock('../../signals/rule_status_service'); jest.mock('../../signals/rule_status_service');
describe('find_rules', () => { describe('find_rules', () => {
@ -27,7 +28,8 @@ describe('find_rules', () => {
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit());
clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams()));
clients.savedObjectsClient.find.mockResolvedValue(getFindBulkResultStatus()); clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse());
clients.ruleExecutionLogClient.findBulk.mockResolvedValue(getFindBulkResultStatus());
findRulesRoute(server.router); findRulesRoute(server.router);
}); });

View file

@ -16,7 +16,6 @@ import type { SecuritySolutionPluginRouter } from '../../../../types';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { findRules } from '../../rules/find_rules'; import { findRules } from '../../rules/find_rules';
import { buildSiemResponse } from '../utils'; import { buildSiemResponse } from '../utils';
import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation';
import { transformFindAlerts } from './utils'; import { transformFindAlerts } from './utils';
import { getBulkRuleActionsSavedObject } from '../../rule_actions/get_bulk_rule_actions_saved_object'; import { getBulkRuleActionsSavedObject } from '../../rule_actions/get_bulk_rule_actions_saved_object';
@ -53,7 +52,7 @@ export const findRulesRoute = (
return siemResponse.error({ statusCode: 404 }); return siemResponse.error({ statusCode: 404 });
} }
const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const execLogClient = context.securitySolution.getExecutionLogClient();
const rules = await findRules({ const rules = await findRules({
rulesClient, rulesClient,
perPage: query.per_page, perPage: query.per_page,
@ -64,8 +63,13 @@ export const findRulesRoute = (
fields: query.fields, fields: query.fields,
}); });
const alertIds = rules.data.map((rule) => rule.id); const alertIds = rules.data.map((rule) => rule.id);
const [ruleStatuses, ruleActions] = await Promise.all([ const [ruleStatuses, ruleActions] = await Promise.all([
ruleStatusClient.findBulk(alertIds, 1), execLogClient.findBulk({
ruleIds: alertIds,
logsCount: 1,
spaceId: context.securitySolution.getSpaceId(),
}),
getBulkRuleActionsSavedObject({ alertIds, savedObjectsClient }), getBulkRuleActionsSavedObject({ alertIds, savedObjectsClient }),
]); ]);
const transformed = transformFindAlerts(rules, ruleActions, ruleStatuses); const transformed = transformFindAlerts(rules, ruleActions, ruleStatuses);

View file

@ -26,7 +26,7 @@ describe('find_statuses', () => {
beforeEach(async () => { beforeEach(async () => {
server = serverMock.create(); server = serverMock.create();
({ clients, context } = requestContextMock.createTools()); ({ clients, context } = requestContextMock.createTools());
clients.savedObjectsClient.find.mockResolvedValue(getFindBulkResultStatus()); // successful status search clients.ruleExecutionLogClient.findBulk.mockResolvedValue(getFindBulkResultStatus()); // successful status search
clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams()));
findRulesStatusesRoute(server.router); findRulesStatusesRoute(server.router);
}); });
@ -45,7 +45,7 @@ describe('find_statuses', () => {
}); });
test('catch error when status search throws error', async () => { test('catch error when status search throws error', async () => {
clients.savedObjectsClient.find.mockImplementation(async () => { clients.ruleExecutionLogClient.findBulk.mockImplementation(async () => {
throw new Error('Test error'); throw new Error('Test error');
}); });
const response = await server.inject(ruleStatusRequest(), context); const response = await server.inject(ruleStatusRequest(), context);

View file

@ -10,7 +10,6 @@ import { buildRouteValidation } from '../../../../utils/build_validation/route_v
import type { SecuritySolutionPluginRouter } from '../../../../types'; import type { SecuritySolutionPluginRouter } from '../../../../types';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { buildSiemResponse, mergeStatuses, getFailingRules } from '../utils'; import { buildSiemResponse, mergeStatuses, getFailingRules } from '../utils';
import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
import { import {
findRulesStatusesSchema, findRulesStatusesSchema,
FindRulesStatusesSchemaDecoded, FindRulesStatusesSchemaDecoded,
@ -41,7 +40,6 @@ export const findRulesStatusesRoute = (router: SecuritySolutionPluginRouter) =>
const { body } = request; const { body } = request;
const siemResponse = buildSiemResponse(response); const siemResponse = buildSiemResponse(response);
const rulesClient = context.alerting?.getRulesClient(); const rulesClient = context.alerting?.getRulesClient();
const savedObjectsClient = context.core.savedObjects.client;
if (!rulesClient) { if (!rulesClient) {
return siemResponse.error({ statusCode: 404 }); return siemResponse.error({ statusCode: 404 });
@ -49,9 +47,13 @@ export const findRulesStatusesRoute = (router: SecuritySolutionPluginRouter) =>
const ids = body.ids; const ids = body.ids;
try { try {
const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const ruleStatusClient = context.securitySolution.getExecutionLogClient();
const [statusesById, failingRules] = await Promise.all([ const [statusesById, failingRules] = await Promise.all([
ruleStatusClient.findBulk(ids, 6), ruleStatusClient.findBulk({
ruleIds: ids,
logsCount: 6,
spaceId: context.securitySolution.getSpaceId(),
}),
getFailingRules(ids, rulesClient), getFailingRules(ids, rulesClient),
]); ]);

View file

@ -92,6 +92,7 @@ export const importRulesRoute = (
savedObjectsClient, savedObjectsClient,
}); });
const ruleStatusClient = context.securitySolution.getExecutionLogClient();
const { filename } = (request.body.file as HapiReadableStream).hapi; const { filename } = (request.body.file as HapiReadableStream).hapi;
const fileExtension = extname(filename).toLowerCase(); const fileExtension = extname(filename).toLowerCase();
if (fileExtension !== '.ndjson') { if (fileExtension !== '.ndjson') {
@ -259,7 +260,8 @@ export const importRulesRoute = (
rulesClient, rulesClient,
author, author,
buildingBlockType, buildingBlockType,
savedObjectsClient, spaceId: context.securitySolution.getSpaceId(),
ruleStatusClient,
description, description,
enabled, enabled,
eventCategoryOverride, eventCategoryOverride,

View file

@ -23,7 +23,6 @@ import { getIdBulkError } from './utils';
import { transformValidateBulkError } from './validate'; import { transformValidateBulkError } from './validate';
import { patchRules } from '../../rules/patch_rules'; import { patchRules } from '../../rules/patch_rules';
import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { updateRulesNotifications } from '../../rules/update_rules_notifications';
import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
import { readRules } from '../../rules/read_rules'; import { readRules } from '../../rules/read_rules';
import { PartialFilter } from '../../types'; import { PartialFilter } from '../../types';
@ -47,6 +46,7 @@ export const patchRulesBulkRoute = (
const siemResponse = buildSiemResponse(response); const siemResponse = buildSiemResponse(response);
const rulesClient = context.alerting?.getRulesClient(); const rulesClient = context.alerting?.getRulesClient();
const ruleStatusClient = context.securitySolution.getExecutionLogClient();
const savedObjectsClient = context.core.savedObjects.client; const savedObjectsClient = context.core.savedObjects.client;
if (!rulesClient) { if (!rulesClient) {
@ -59,7 +59,6 @@ export const patchRulesBulkRoute = (
request, request,
savedObjectsClient, savedObjectsClient,
}); });
const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
const rules = await Promise.all( const rules = await Promise.all(
request.body.map(async (payloadRule) => { request.body.map(async (payloadRule) => {
const { const {
@ -144,7 +143,8 @@ export const patchRulesBulkRoute = (
license, license,
outputIndex, outputIndex,
savedId, savedId,
savedObjectsClient, spaceId: context.securitySolution.getSpaceId(),
ruleStatusClient,
timelineId, timelineId,
timelineTitle, timelineTitle,
meta, meta,
@ -190,11 +190,9 @@ export const patchRulesBulkRoute = (
name: rule.name, name: rule.name,
}); });
const ruleStatuses = await ruleStatusClient.find({ const ruleStatuses = await ruleStatusClient.find({
perPage: 1, logsCount: 1,
sortField: 'statusDate', ruleId: rule.id,
sortOrder: 'desc', spaceId: context.securitySolution.getSpaceId(),
search: rule.id,
searchFields: ['alertId'],
}); });
return transformValidateBulkError(rule.id, rule, ruleActions, ruleStatuses); return transformValidateBulkError(rule.id, rule, ruleActions, ruleStatuses);
} else { } else {

View file

@ -10,12 +10,13 @@ import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../mach
import { buildMlAuthz } from '../../../machine_learning/authz'; import { buildMlAuthz } from '../../../machine_learning/authz';
import { import {
getEmptyFindResult, getEmptyFindResult,
getFindResultStatus, getRuleExecutionStatuses,
getAlertMock, getAlertMock,
getPatchRequest, getPatchRequest,
getFindResultWithSingleHit, getFindResultWithSingleHit,
nonRuleFindResult, nonRuleFindResult,
typicalMlRulePayload, typicalMlRulePayload,
getEmptySavedObjectsResponse,
} from '../__mocks__/request_responses'; } from '../__mocks__/request_responses';
import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { requestContextMock, serverMock, requestMock } from '../__mocks__';
import { patchRulesRoute } from './patch_rules_route'; import { patchRulesRoute } from './patch_rules_route';
@ -37,7 +38,9 @@ describe('patch_rules', () => {
clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); // existing rule clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); // existing rule
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // existing rule clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // existing rule
clients.rulesClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams())); // successful update clients.rulesClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams())); // successful update
clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); // successful transform clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); // successful transform
clients.savedObjectsClient.create.mockResolvedValue(getRuleExecutionStatuses()[0]); // successful transform
clients.ruleExecutionLogClient.find.mockResolvedValue(getRuleExecutionStatuses());
patchRulesRoute(server.router, ml); patchRulesRoute(server.router, ml);
}); });

View file

@ -25,7 +25,6 @@ import { buildSiemResponse } from '../utils';
import { getIdError } from './utils'; import { getIdError } from './utils';
import { transformValidate } from './validate'; import { transformValidate } from './validate';
import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { updateRulesNotifications } from '../../rules/update_rules_notifications';
import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
import { readRules } from '../../rules/read_rules'; import { readRules } from '../../rules/read_rules';
import { PartialFilter } from '../../types'; import { PartialFilter } from '../../types';
@ -108,6 +107,7 @@ export const patchRulesRoute = (
const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[];
const rulesClient = context.alerting?.getRulesClient(); const rulesClient = context.alerting?.getRulesClient();
const ruleStatusClient = context.securitySolution.getExecutionLogClient();
const savedObjectsClient = context.core.savedObjects.client; const savedObjectsClient = context.core.savedObjects.client;
if (!rulesClient) { if (!rulesClient) {
@ -131,7 +131,6 @@ export const patchRulesRoute = (
throwHttpError(await mlAuthz.validateRuleType(existingRule?.params.type)); throwHttpError(await mlAuthz.validateRuleType(existingRule?.params.type));
} }
const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
const rule = await patchRules({ const rule = await patchRules({
rulesClient, rulesClient,
author, author,
@ -146,7 +145,8 @@ export const patchRulesRoute = (
license, license,
outputIndex, outputIndex,
savedId, savedId,
savedObjectsClient, spaceId: context.securitySolution.getSpaceId(),
ruleStatusClient,
timelineId, timelineId,
timelineTitle, timelineTitle,
meta, meta,
@ -193,18 +193,12 @@ export const patchRulesRoute = (
name: rule.name, name: rule.name,
}); });
const ruleStatuses = await ruleStatusClient.find({ const ruleStatuses = await ruleStatusClient.find({
perPage: 1, logsCount: 1,
sortField: 'statusDate', ruleId: rule.id,
sortOrder: 'desc', spaceId: context.securitySolution.getSpaceId(),
search: rule.id,
searchFields: ['alertId'],
}); });
const [validated, errors] = transformValidate( const [validated, errors] = transformValidate(rule, ruleActions, ruleStatuses[0]);
rule,
ruleActions,
ruleStatuses.saved_objects[0]
);
if (errors != null) { if (errors != null) {
return siemResponse.error({ statusCode: 500, body: errors }); return siemResponse.error({ statusCode: 500, body: errors });
} else { } else {

View file

@ -10,7 +10,6 @@ import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../mach
import { buildMlAuthz } from '../../../machine_learning/authz'; import { buildMlAuthz } from '../../../machine_learning/authz';
import { import {
getEmptyFindResult, getEmptyFindResult,
getFindResultStatus,
getBulkActionRequest, getBulkActionRequest,
getFindResultWithSingleHit, getFindResultWithSingleHit,
getFindResultWithMultiHits, getFindResultWithMultiHits,
@ -32,7 +31,6 @@ describe('perform_bulk_action', () => {
ml = mlServicesMock.createSetupContract(); ml = mlServicesMock.createSetupContract();
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit());
clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
performBulkActionRoute(server.router, ml); performBulkActionRoute(server.router, ml);
}); });

View file

@ -21,7 +21,6 @@ import { findRules } from '../../rules/find_rules';
import { getExportByObjectIds } from '../../rules/get_export_by_object_ids'; import { getExportByObjectIds } from '../../rules/get_export_by_object_ids';
import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { updateRulesNotifications } from '../../rules/update_rules_notifications';
import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object';
import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
import { buildSiemResponse } from '../utils'; import { buildSiemResponse } from '../utils';
const BULK_ACTION_RULES_LIMIT = 10000; const BULK_ACTION_RULES_LIMIT = 10000;
@ -47,7 +46,7 @@ export const performBulkActionRoute = (
try { try {
const rulesClient = context.alerting?.getRulesClient(); const rulesClient = context.alerting?.getRulesClient();
const savedObjectsClient = context.core.savedObjects.client; const savedObjectsClient = context.core.savedObjects.client;
const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const ruleStatusClient = context.securitySolution.getExecutionLogClient();
const mlAuthz = buildMlAuthz({ const mlAuthz = buildMlAuthz({
license: context.licensing.license, license: context.licensing.license,
@ -83,7 +82,12 @@ export const performBulkActionRoute = (
rules.data.map(async (rule) => { rules.data.map(async (rule) => {
if (!rule.enabled) { if (!rule.enabled) {
throwHttpError(await mlAuthz.validateRuleType(rule.params.type)); throwHttpError(await mlAuthz.validateRuleType(rule.params.type));
await enableRule({ rule, rulesClient, savedObjectsClient }); await enableRule({
rule,
rulesClient,
ruleStatusClient,
spaceId: context.securitySolution.getSpaceId(),
});
} }
}) })
); );
@ -102,9 +106,9 @@ export const performBulkActionRoute = (
await Promise.all( await Promise.all(
rules.data.map(async (rule) => { rules.data.map(async (rule) => {
const ruleStatuses = await ruleStatusClient.find({ const ruleStatuses = await ruleStatusClient.find({
perPage: 6, logsCount: 6,
search: rule.id, ruleId: rule.id,
searchFields: ['alertId'], spaceId: context.securitySolution.getSpaceId(),
}); });
await deleteRules({ await deleteRules({
rulesClient, rulesClient,

View file

@ -12,7 +12,7 @@ import {
getReadRequest, getReadRequest,
getFindResultWithSingleHit, getFindResultWithSingleHit,
nonRuleFindResult, nonRuleFindResult,
getFindResultStatusEmpty, getEmptySavedObjectsResponse,
} from '../__mocks__/request_responses'; } from '../__mocks__/request_responses';
import { requestMock, requestContextMock, serverMock } from '../__mocks__'; import { requestMock, requestContextMock, serverMock } from '../__mocks__';
@ -25,7 +25,8 @@ describe('read_signals', () => {
({ clients, context } = requestContextMock.createTools()); ({ clients, context } = requestContextMock.createTools());
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists
clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatusEmpty()); // successful transform clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); // successful transform
clients.ruleExecutionLogClient.find.mockResolvedValue([]);
readRulesRoute(server.router); readRulesRoute(server.router);
}); });

View file

@ -20,7 +20,7 @@ import { buildSiemResponse } from '../utils';
import { readRules } from '../../rules/read_rules'; import { readRules } from '../../rules/read_rules';
import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object';
import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
export const readRulesRoute = ( export const readRulesRoute = (
router: SecuritySolutionPluginRouter, router: SecuritySolutionPluginRouter,
@ -55,7 +55,7 @@ export const readRulesRoute = (
return siemResponse.error({ statusCode: 404 }); return siemResponse.error({ statusCode: 404 });
} }
const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const ruleStatusClient = context.securitySolution.getExecutionLogClient();
const rule = await readRules({ const rule = await readRules({
rulesClient, rulesClient,
id, id,
@ -67,18 +67,16 @@ export const readRulesRoute = (
ruleAlertId: rule.id, ruleAlertId: rule.id,
}); });
const ruleStatuses = await ruleStatusClient.find({ const ruleStatuses = await ruleStatusClient.find({
perPage: 1, logsCount: 1,
sortField: 'statusDate', ruleId: rule.id,
sortOrder: 'desc', spaceId: context.securitySolution.getSpaceId(),
search: rule.id,
searchFields: ['alertId'],
}); });
const [currentStatus] = ruleStatuses.saved_objects; const [currentStatus] = ruleStatuses;
if (currentStatus != null && rule.executionStatus.status === 'error') { if (currentStatus != null && rule.executionStatus.status === 'error') {
currentStatus.attributes.lastFailureMessage = `Reason: ${rule.executionStatus.error?.reason} Message: ${rule.executionStatus.error?.message}`; currentStatus.attributes.lastFailureMessage = `Reason: ${rule.executionStatus.error?.reason} Message: ${rule.executionStatus.error?.message}`;
currentStatus.attributes.lastFailureAt = rule.executionStatus.lastExecutionDate.toISOString(); currentStatus.attributes.lastFailureAt = rule.executionStatus.lastExecutionDate.toISOString();
currentStatus.attributes.statusDate = rule.executionStatus.lastExecutionDate.toISOString(); currentStatus.attributes.statusDate = rule.executionStatus.lastExecutionDate.toISOString();
currentStatus.attributes.status = 'failed'; currentStatus.attributes.status = RuleExecutionStatus.failed;
} }
const transformed = transform(rule, ruleActions, currentStatus); const transformed = transform(rule, ruleActions, currentStatus);
if (transformed == null) { if (transformed == null) {

View file

@ -13,7 +13,6 @@ import {
getAlertMock, getAlertMock,
getFindResultWithSingleHit, getFindResultWithSingleHit,
getUpdateBulkRequest, getUpdateBulkRequest,
getFindResultStatus,
typicalMlRulePayload, typicalMlRulePayload,
} from '../__mocks__/request_responses'; } from '../__mocks__/request_responses';
import { serverMock, requestContextMock, requestMock } from '../__mocks__'; import { serverMock, requestContextMock, requestMock } from '../__mocks__';
@ -36,7 +35,6 @@ describe('update_rules_bulk', () => {
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit());
clients.rulesClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams())); clients.rulesClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams()));
clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
updateRulesBulkRoute(server.router, ml); updateRulesBulkRoute(server.router, ml);
}); });

View file

@ -20,7 +20,6 @@ import { transformValidateBulkError } from './validate';
import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '../utils'; import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '../utils';
import { updateRules } from '../../rules/update_rules'; import { updateRules } from '../../rules/update_rules';
import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { updateRulesNotifications } from '../../rules/update_rules_notifications';
import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
export const updateRulesBulkRoute = ( export const updateRulesBulkRoute = (
router: SecuritySolutionPluginRouter, router: SecuritySolutionPluginRouter,
@ -54,7 +53,7 @@ export const updateRulesBulkRoute = (
savedObjectsClient, savedObjectsClient,
}); });
const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const ruleStatusClient = context.securitySolution.getExecutionLogClient();
const rules = await Promise.all( const rules = await Promise.all(
request.body.map(async (payloadRule) => { request.body.map(async (payloadRule) => {
const idOrRuleIdOrUnknown = payloadRule.id ?? payloadRule.rule_id ?? '(unknown id)'; const idOrRuleIdOrUnknown = payloadRule.id ?? payloadRule.rule_id ?? '(unknown id)';
@ -71,8 +70,9 @@ export const updateRulesBulkRoute = (
throwHttpError(await mlAuthz.validateRuleType(payloadRule.type)); throwHttpError(await mlAuthz.validateRuleType(payloadRule.type));
const rule = await updateRules({ const rule = await updateRules({
spaceId: context.securitySolution.getSpaceId(),
rulesClient, rulesClient,
savedObjectsClient, ruleStatusClient,
defaultOutputIndex: siemClient.getSignalsIndex(), defaultOutputIndex: siemClient.getSignalsIndex(),
ruleUpdate: payloadRule, ruleUpdate: payloadRule,
}); });
@ -87,11 +87,9 @@ export const updateRulesBulkRoute = (
name: payloadRule.name, name: payloadRule.name,
}); });
const ruleStatuses = await ruleStatusClient.find({ const ruleStatuses = await ruleStatusClient.find({
perPage: 1, logsCount: 1,
sortField: 'statusDate', ruleId: rule.id,
sortOrder: 'desc', spaceId: context.securitySolution.getSpaceId(),
search: rule.id,
searchFields: ['alertId'],
}); });
return transformValidateBulkError(rule.id, rule, ruleActions, ruleStatuses); return transformValidateBulkError(rule.id, rule, ruleActions, ruleStatuses);
} else { } else {

View file

@ -12,7 +12,6 @@ import {
getAlertMock, getAlertMock,
getUpdateRequest, getUpdateRequest,
getFindResultWithSingleHit, getFindResultWithSingleHit,
getFindResultStatusEmpty,
nonRuleFindResult, nonRuleFindResult,
typicalMlRulePayload, typicalMlRulePayload,
} from '../__mocks__/request_responses'; } from '../__mocks__/request_responses';
@ -39,7 +38,7 @@ describe('update_rules', () => {
clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); // existing rule clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); // existing rule
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists
clients.rulesClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams())); // successful update clients.rulesClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams())); // successful update
clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatusEmpty()); // successful transform clients.ruleExecutionLogClient.find.mockResolvedValue([]); // successful transform: ;
updateRulesRoute(server.router, ml); updateRulesRoute(server.router, ml);
}); });

View file

@ -20,7 +20,6 @@ import { getIdError } from './utils';
import { transformValidate } from './validate'; import { transformValidate } from './validate';
import { updateRules } from '../../rules/update_rules'; import { updateRules } from '../../rules/update_rules';
import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { updateRulesNotifications } from '../../rules/update_rules_notifications';
import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation';
export const updateRulesRoute = ( export const updateRulesRoute = (
@ -48,7 +47,6 @@ export const updateRulesRoute = (
const rulesClient = context.alerting?.getRulesClient(); const rulesClient = context.alerting?.getRulesClient();
const savedObjectsClient = context.core.savedObjects.client; const savedObjectsClient = context.core.savedObjects.client;
const siemClient = context.securitySolution?.getAppClient(); const siemClient = context.securitySolution?.getAppClient();
const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
if (!siemClient || !rulesClient) { if (!siemClient || !rulesClient) {
return siemResponse.error({ statusCode: 404 }); return siemResponse.error({ statusCode: 404 });
@ -62,9 +60,11 @@ export const updateRulesRoute = (
}); });
throwHttpError(await mlAuthz.validateRuleType(request.body.type)); throwHttpError(await mlAuthz.validateRuleType(request.body.type));
const ruleStatusClient = context.securitySolution.getExecutionLogClient();
const rule = await updateRules({ const rule = await updateRules({
spaceId: context.securitySolution.getSpaceId(),
rulesClient, rulesClient,
savedObjectsClient, ruleStatusClient,
defaultOutputIndex: siemClient.getSignalsIndex(), defaultOutputIndex: siemClient.getSignalsIndex(),
ruleUpdate: request.body, ruleUpdate: request.body,
}); });
@ -80,17 +80,11 @@ export const updateRulesRoute = (
name: request.body.name, name: request.body.name,
}); });
const ruleStatuses = await ruleStatusClient.find({ const ruleStatuses = await ruleStatusClient.find({
perPage: 1, logsCount: 1,
sortField: 'statusDate', ruleId: rule.id,
sortOrder: 'desc', spaceId: context.securitySolution.getSpaceId(),
search: rule.id,
searchFields: ['alertId'],
}); });
const [validated, errors] = transformValidate( const [validated, errors] = transformValidate(rule, ruleActions, ruleStatuses[0]);
rule,
ruleActions,
ruleStatuses.saved_objects[0]
);
if (errors != null) { if (errors != null) {
return siemResponse.error({ statusCode: 500, body: errors }); return siemResponse.error({ statusCode: 500, body: errors });
} else { } else {

View file

@ -8,10 +8,11 @@
import { transformValidate, transformValidateBulkError } from './validate'; import { transformValidate, transformValidateBulkError } from './validate';
import { BulkError } from '../utils'; import { BulkError } from '../utils';
import { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response';
import { getAlertMock, getFindResultStatus } from '../__mocks__/request_responses'; import { getAlertMock, getRuleExecutionStatuses } from '../__mocks__/request_responses';
import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock';
import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock';
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
export const ruleOutput = (): RulesSchema => ({ export const ruleOutput = (): RulesSchema => ({
actions: [], actions: [],
@ -107,12 +108,12 @@ describe('validate', () => {
}); });
test('it should do a validation correctly of a rule id with ruleStatus passed in', () => { test('it should do a validation correctly of a rule id with ruleStatus passed in', () => {
const ruleStatus = getFindResultStatus(); const ruleStatuses = getRuleExecutionStatuses();
const ruleAlert = getAlertMock(getQueryRuleParams()); const ruleAlert = getAlertMock(getQueryRuleParams());
const validatedOrError = transformValidateBulkError('rule-1', ruleAlert, null, ruleStatus); const validatedOrError = transformValidateBulkError('rule-1', ruleAlert, null, ruleStatuses);
const expected: RulesSchema = { const expected: RulesSchema = {
...ruleOutput(), ...ruleOutput(),
status: 'succeeded', status: RuleExecutionStatus.succeeded,
status_date: '2020-02-18T15:26:49.783Z', status_date: '2020-02-18T15:26:49.783Z',
last_success_at: '2020-02-18T15:26:49.783Z', last_success_at: '2020-02-18T15:26:49.783Z',
last_success_message: 'succeeded', last_success_message: 'succeeded',

View file

@ -5,7 +5,7 @@
* 2.0. * 2.0.
*/ */
import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; import { SavedObject, SavedObjectsFindResult } from 'kibana/server';
import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; import { validateNonExact } from '@kbn/securitysolution-io-ts-utils';
import { import {
@ -20,8 +20,8 @@ import { PartialAlert } from '../../../../../../alerting/server';
import { import {
isAlertType, isAlertType,
IRuleSavedAttributesSavedObjectAttributes, IRuleSavedAttributesSavedObjectAttributes,
isRuleStatusFindType,
IRuleStatusSOAttributes, IRuleStatusSOAttributes,
isRuleStatusSavedObjectType,
} from '../../rules/types'; } from '../../rules/types';
import { createBulkErrorObject, BulkError } from '../utils'; import { createBulkErrorObject, BulkError } from '../utils';
import { transform, transformAlertToRule } from './utils'; import { transform, transformAlertToRule } from './utils';
@ -58,15 +58,11 @@ export const transformValidateBulkError = (
ruleId: string, ruleId: string,
alert: PartialAlert<RuleParams>, alert: PartialAlert<RuleParams>,
ruleActions?: RuleActions | null, ruleActions?: RuleActions | null,
ruleStatus?: SavedObjectsFindResponse<IRuleStatusSOAttributes> ruleStatus?: Array<SavedObjectsFindResult<IRuleStatusSOAttributes>>
): RulesSchema | BulkError => { ): RulesSchema | BulkError => {
if (isAlertType(alert)) { if (isAlertType(alert)) {
if (isRuleStatusFindType(ruleStatus) && ruleStatus?.saved_objects.length > 0) { if (ruleStatus && ruleStatus?.length > 0 && isRuleStatusSavedObjectType(ruleStatus[0])) {
const transformed = transformAlertToRule( const transformed = transformAlertToRule(alert, ruleActions, ruleStatus[0]);
alert,
ruleActions,
ruleStatus?.saved_objects[0] ?? ruleStatus
);
const [validated, errors] = validateNonExact(transformed, rulesSchema); const [validated, errors] = validateNonExact(transformed, rulesSchema);
if (errors != null || validated == null) { if (errors != null || validated == null) {
return createBulkErrorObject({ return createBulkErrorObject({

View file

@ -29,6 +29,7 @@ import { exampleRuleStatus } from '../signals/__mocks__/es_results';
import { getAlertMock } from './__mocks__/request_responses'; import { getAlertMock } from './__mocks__/request_responses';
import { AlertExecutionStatusErrorReasons } from '../../../../../alerting/common'; import { AlertExecutionStatusErrorReasons } from '../../../../../alerting/common';
import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; import { getQueryRuleParams } from '../schemas/rule_schemas.mock';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
let rulesClient: ReturnType<typeof rulesClientMock.create>; let rulesClient: ReturnType<typeof rulesClientMock.create>;
@ -297,9 +298,9 @@ describe('utils', () => {
describe('mergeStatuses', () => { describe('mergeStatuses', () => {
it('merges statuses and converts from camelCase saved object to snake_case HTTP response', () => { it('merges statuses and converts from camelCase saved object to snake_case HTTP response', () => {
const statusOne = exampleRuleStatus(); const statusOne = exampleRuleStatus();
statusOne.attributes.status = 'failed'; statusOne.attributes.status = RuleExecutionStatus.failed;
const statusTwo = exampleRuleStatus(); const statusTwo = exampleRuleStatus();
statusTwo.attributes.status = 'failed'; statusTwo.attributes.status = RuleExecutionStatus.failed;
const currentStatus = exampleRuleStatus(); const currentStatus = exampleRuleStatus();
const foundRules = [currentStatus.attributes, statusOne.attributes, statusTwo.attributes]; const foundRules = [currentStatus.attributes, statusOne.attributes, statusTwo.attributes];
const res = mergeStatuses(currentStatus.attributes.alertId, foundRules, { const res = mergeStatuses(currentStatus.attributes.alertId, foundRules, {
@ -307,7 +308,7 @@ describe('utils', () => {
current_status: { current_status: {
alert_id: 'myfakealertid-8cfac', alert_id: 'myfakealertid-8cfac',
status_date: '2020-03-27T22:55:59.517Z', status_date: '2020-03-27T22:55:59.517Z',
status: 'succeeded', status: RuleExecutionStatus.succeeded,
last_failure_at: null, last_failure_at: null,
last_success_at: '2020-03-27T22:55:59.517Z', last_success_at: '2020-03-27T22:55:59.517Z',
last_failure_message: null, last_failure_message: null,

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { IRuleExecutionLogClient } from '../types';
export const RuleExecutionLogClient = jest
.fn<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(),
};
});

View file

@ -0,0 +1,105 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { merge } from 'lodash';
import { RuleDataPluginService } from '../../../../../../rule_registry/server';
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
import { IRuleStatusSOAttributes } from '../../rules/types';
import { RuleRegistryLogClient } from '../rule_registry_log_client/rule_registry_log_client';
import {
ExecutionMetric,
ExecutionMetricArgs,
FindBulkExecutionLogArgs,
FindExecutionLogArgs,
IRuleExecutionLogClient,
LogStatusChangeArgs,
} from '../types';
/**
* @deprecated RuleRegistryAdapter is kept here only as a reference. It will be superseded with EventLog implementation
*/
export class RuleRegistryAdapter implements IRuleExecutionLogClient {
private ruleRegistryClient: RuleRegistryLogClient;
constructor(ruleDataService: RuleDataPluginService) {
this.ruleRegistryClient = new RuleRegistryLogClient(ruleDataService);
}
public async find({ ruleId, logsCount = 1, spaceId }: FindExecutionLogArgs) {
const logs = await this.ruleRegistryClient.find({
ruleIds: [ruleId],
logsCount,
spaceId,
});
return logs[ruleId].map((log) => ({
id: '',
type: '',
score: 0,
attributes: log,
references: [],
}));
}
public async findBulk({ ruleIds, logsCount = 1, spaceId }: FindBulkExecutionLogArgs) {
const [statusesById, lastErrorsById] = await Promise.all([
this.ruleRegistryClient.find({ ruleIds, spaceId }),
this.ruleRegistryClient.find({
ruleIds,
statuses: [RuleExecutionStatus.failed],
logsCount,
spaceId,
}),
]);
return merge(statusesById, lastErrorsById);
}
public async create(event: IRuleStatusSOAttributes, spaceId: string) {
if (event.status) {
await this.ruleRegistryClient.logStatusChange({
ruleId: event.alertId,
newStatus: event.status,
spaceId,
});
}
if (event.bulkCreateTimeDurations) {
await this.ruleRegistryClient.logExecutionMetric({
ruleId: event.alertId,
metric: ExecutionMetric.indexingDurationMax,
value: Math.max(...event.bulkCreateTimeDurations.map(Number)),
spaceId,
});
}
if (event.gap) {
await this.ruleRegistryClient.logExecutionMetric({
ruleId: event.alertId,
metric: ExecutionMetric.executionGap,
value: Number(event.gap),
spaceId,
});
}
}
public async update(id: string, event: IRuleStatusSOAttributes, spaceId: string) {
// execution events are immutable, so we just use 'create' here instead of 'update'
await this.create(event, spaceId);
}
public async delete(id: string) {
// execution events are immutable, nothing to do here
}
public async logExecutionMetric<T extends ExecutionMetric>(args: ExecutionMetricArgs<T>) {
return this.ruleRegistryClient.logExecutionMetric(args);
}
public async logStatusChange(args: LogStatusChangeArgs) {
return this.ruleRegistryClient.logStatusChange(args);
}
}

View file

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

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObjectsClientContract } from '../../../../../../../src/core/server';
import { RuleDataPluginService } from '../../../../../rule_registry/server';
import { IRuleStatusSOAttributes } from '../rules/types';
import { RuleRegistryAdapter } from './adapters/rule_registry_dapter';
import { SavedObjectsAdapter } from './adapters/saved_objects_adapter';
import {
ExecutionMetric,
ExecutionMetricArgs,
FindBulkExecutionLogArgs,
FindExecutionLogArgs,
IRuleExecutionLogClient,
LogStatusChangeArgs,
} from './types';
export interface RuleExecutionLogClientArgs {
ruleDataService: RuleDataPluginService;
savedObjectsClient: SavedObjectsClientContract;
}
const RULE_REGISTRY_LOG_ENABLED = false;
export class RuleExecutionLogClient implements IRuleExecutionLogClient {
private client: IRuleExecutionLogClient;
constructor({ ruleDataService, savedObjectsClient }: RuleExecutionLogClientArgs) {
if (RULE_REGISTRY_LOG_ENABLED) {
this.client = new RuleRegistryAdapter(ruleDataService);
} else {
this.client = new SavedObjectsAdapter(savedObjectsClient);
}
}
public find(args: FindExecutionLogArgs) {
return this.client.find(args);
}
public findBulk(args: FindBulkExecutionLogArgs) {
return this.client.findBulk(args);
}
// TODO args as an object
public async create(event: IRuleStatusSOAttributes, spaceId: string) {
return this.client.create(event, spaceId);
}
// TODO args as an object
public async update(id: string, event: IRuleStatusSOAttributes, spaceId: string) {
return this.client.update(id, event, spaceId);
}
public async delete(id: string) {
return this.client.delete(id);
}
public async logExecutionMetric<T extends ExecutionMetric>(args: ExecutionMetricArgs<T>) {
return this.client.logExecutionMetric(args);
}
public async logStatusChange(args: LogStatusChangeArgs) {
return this.client.logStatusChange(args);
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/**
* @deprecated EVENTS_INDEX_PREFIX is kept here only as a reference. It will be superseded with EventLog implementation
*/
export const EVENTS_INDEX_PREFIX = '.kibana_alerts-security.events';
/**
* @deprecated MESSAGE is kept here only as a reference. It will be superseded with EventLog implementation
*/
export const MESSAGE = 'message' as const;
/**
* @deprecated EVENT_SEQUENCE is kept here only as a reference. It will be superseded with EventLog implementation
*/
export const EVENT_SEQUENCE = 'event.sequence' as const;
/**
* @deprecated EVENT_DURATION is kept here only as a reference. It will be superseded with EventLog implementation
*/
export const EVENT_DURATION = 'event.duration' as const;
/**
* @deprecated EVENT_END is kept here only as a reference. It will be superseded with EventLog implementation
*/
export const EVENT_END = 'event.end' as const;
/**
* @deprecated RULE_STATUS is kept here only as a reference. It will be superseded with EventLog implementation
*/
export const RULE_STATUS = 'kibana.rac.detection_engine.rule_status' as const;
/**
* @deprecated RULE_STATUS_SEVERITY is kept here only as a reference. It will be superseded with EventLog implementation
*/
export const RULE_STATUS_SEVERITY = 'kibana.rac.detection_engine.rule_status_severity' as const;

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isLeft } from 'fp-ts/lib/Either';
import { PathReporter } from 'io-ts/lib/PathReporter';
import { technicalRuleFieldMap } from '../../../../../../rule_registry/common/assets/field_maps/technical_rule_field_map';
import {
mergeFieldMaps,
runtimeTypeFromFieldMap,
} from '../../../../../../rule_registry/common/field_map';
import { ruleExecutionFieldMap } from './rule_execution_field_map';
const ruleExecutionLogRuntimeType = runtimeTypeFromFieldMap(
mergeFieldMaps(technicalRuleFieldMap, ruleExecutionFieldMap)
);
/**
* @deprecated parseRuleExecutionLog is kept here only as a reference. It will be superseded with EventLog implementation
*/
export const parseRuleExecutionLog = (input: unknown) => {
const validate = ruleExecutionLogRuntimeType.decode(input);
if (isLeft(validate)) {
throw new Error(PathReporter.report(validate).join('\n'));
}
return ruleExecutionLogRuntimeType.encode(validate.right);
};
/**
* @deprecated RuleExecutionEvent is kept here only as a reference. It will be superseded with EventLog implementation
*/
export type RuleExecutionEvent = ReturnType<typeof parseRuleExecutionLog>;

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EVENT_DURATION,
EVENT_END,
EVENT_SEQUENCE,
MESSAGE,
RULE_STATUS,
RULE_STATUS_SEVERITY,
} from './constants';
/**
* @deprecated ruleExecutionFieldMap is kept here only as a reference. It will be superseded with EventLog implementation
*/
export const ruleExecutionFieldMap = {
[MESSAGE]: { type: 'keyword' },
[EVENT_SEQUENCE]: { type: 'long' },
[EVENT_END]: { type: 'date' },
[EVENT_DURATION]: { type: 'long' },
[RULE_STATUS]: { type: 'keyword' },
[RULE_STATUS_SEVERITY]: { type: 'integer' },
} as const;
/**
* @deprecated RuleExecutionFieldMap is kept here only as a reference. It will be superseded with EventLog implementation
*/
export type RuleExecutionFieldMap = typeof ruleExecutionFieldMap;

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../../../../../rule_registry/common/assets';
import { mappingFromFieldMap } from '../../../../../../rule_registry/common/mapping_from_field_map';
import { RuleDataPluginService } from '../../../../../../rule_registry/server';
import { ruleExecutionFieldMap } from './rule_execution_field_map';
/**
* @deprecated bootstrapRuleExecutionLog is kept here only as a reference. It will be superseded with EventLog implementation
*/
export const bootstrapRuleExecutionLog = async (
ruleDataService: RuleDataPluginService,
indexAlias: string
) => {
const indexPattern = `${indexAlias}*`;
const componentTemplateName = `${indexAlias}-mappings`;
const indexTemplateName = `${indexAlias}-template`;
await ruleDataService.createOrUpdateComponentTemplate({
name: componentTemplateName,
body: {
template: {
settings: {
number_of_shards: 1,
},
mappings: mappingFromFieldMap(ruleExecutionFieldMap),
},
},
});
await ruleDataService.createOrUpdateIndexTemplate({
name: indexTemplateName,
body: {
index_patterns: [indexPattern],
composed_of: [
ruleDataService.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME),
componentTemplateName,
],
},
});
await ruleDataService.updateIndexMappingsMatchingPattern(indexPattern);
};

View file

@ -0,0 +1,252 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { estypes } from '@elastic/elasticsearch';
import { EVENT_ACTION, EVENT_KIND, RULE_ID, SPACE_IDS, TIMESTAMP } from '@kbn/rule-data-utils';
import { once } from 'lodash/fp';
import moment from 'moment';
import { RuleDataClient, RuleDataPluginService } from '../../../../../../rule_registry/server';
import { SERVER_APP_ID } from '../../../../../common/constants';
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
import { invariant } from '../../../../../common/utils/invariant';
import { IRuleStatusSOAttributes } from '../../rules/types';
import { makeFloatString } from '../../signals/utils';
import { ExecutionMetric, ExecutionMetricArgs, LogStatusChangeArgs } from '../types';
import {
EVENTS_INDEX_PREFIX,
MESSAGE,
EVENT_SEQUENCE,
RULE_STATUS,
RULE_STATUS_SEVERITY,
} from './constants';
import { parseRuleExecutionLog, RuleExecutionEvent } from './parse_rule_execution_log';
import { bootstrapRuleExecutionLog } from './rule_execution_log_bootstrapper';
import {
getLastEntryAggregation,
getMetricAggregation,
getMetricField,
sortByTimeDesc,
} from './utils';
const statusSeverityDict: Record<RuleExecutionStatus, number> = {
[RuleExecutionStatus.succeeded]: 0,
[RuleExecutionStatus['going to run']]: 10,
[RuleExecutionStatus.warning]: 20,
[RuleExecutionStatus['partial failure']]: 20,
[RuleExecutionStatus.failed]: 30,
};
interface FindExecutionLogArgs {
ruleIds: string[];
spaceId: string;
logsCount?: number;
statuses?: RuleExecutionStatus[];
}
interface IRuleRegistryLogClient {
find: (
args: FindExecutionLogArgs
) => Promise<{
[ruleId: string]: IRuleStatusSOAttributes[] | undefined;
}>;
create: (event: RuleExecutionEvent) => Promise<void>;
logStatusChange: (args: LogStatusChangeArgs) => Promise<void>;
logExecutionMetric: <T extends ExecutionMetric>(args: ExecutionMetricArgs<T>) => Promise<void>;
}
/**
* @deprecated RuleRegistryLogClient is kept here only as a reference. It will be superseded with EventLog implementation
*/
export class RuleRegistryLogClient implements IRuleRegistryLogClient {
private sequence = 0;
private ruleDataClient: RuleDataClient;
constructor(ruleDataService: RuleDataPluginService) {
this.ruleDataClient = ruleDataService.getRuleDataClient(
SERVER_APP_ID,
EVENTS_INDEX_PREFIX,
() => this.initialize(ruleDataService, EVENTS_INDEX_PREFIX)
);
}
private initialize = once(async (ruleDataService: RuleDataPluginService, indexAlias: string) => {
await bootstrapRuleExecutionLog(ruleDataService, indexAlias);
});
public async find({ ruleIds, spaceId, statuses, logsCount = 1 }: FindExecutionLogArgs) {
if (ruleIds.length === 0) {
return {};
}
const filter: estypes.QueryDslQueryContainer[] = [
{ terms: { [RULE_ID]: ruleIds } },
{ terms: { [SPACE_IDS]: [spaceId] } },
];
if (statuses) {
filter.push({ terms: { [RULE_STATUS]: statuses } });
}
const result = await this.ruleDataClient.getReader().search({
size: 0,
body: {
query: {
bool: {
filter,
},
},
aggs: {
rules: {
terms: {
field: RULE_ID,
size: ruleIds.length,
},
aggs: {
most_recent_logs: {
top_hits: {
sort: sortByTimeDesc,
size: logsCount,
},
},
last_failure: getLastEntryAggregation(RuleExecutionStatus.failed),
last_success: getLastEntryAggregation(RuleExecutionStatus.succeeded),
execution_gap: getMetricAggregation(ExecutionMetric.executionGap),
search_duration_max: getMetricAggregation(ExecutionMetric.searchDurationMax),
indexing_duration_max: getMetricAggregation(ExecutionMetric.indexingDurationMax),
indexing_lookback: getMetricAggregation(ExecutionMetric.indexingLookback),
},
},
},
},
});
if (result.hits.total.value === 0) {
return {};
}
invariant(result.aggregations, 'Search response should contain aggregations');
return Object.fromEntries(
result.aggregations.rules.buckets.map((bucket) => [
bucket.key,
bucket.most_recent_logs.hits.hits.map<IRuleStatusSOAttributes>((event) => {
const logEntry = parseRuleExecutionLog(event._source);
invariant(logEntry['rule.id'], 'Malformed execution log entry: rule.id field not found');
const lastFailure = bucket.last_failure.event.hits.hits[0]
? parseRuleExecutionLog(bucket.last_failure.event.hits.hits[0]._source)
: undefined;
const lastSuccess = bucket.last_success.event.hits.hits[0]
? parseRuleExecutionLog(bucket.last_success.event.hits.hits[0]._source)
: undefined;
const lookBack = bucket.indexing_lookback.event.hits.hits[0]
? parseRuleExecutionLog(bucket.indexing_lookback.event.hits.hits[0]._source)
: undefined;
const executionGap = bucket.execution_gap.event.hits.hits[0]
? parseRuleExecutionLog(bucket.execution_gap.event.hits.hits[0]._source)[
getMetricField(ExecutionMetric.executionGap)
]
: undefined;
const searchDuration = bucket.search_duration_max.event.hits.hits[0]
? parseRuleExecutionLog(bucket.search_duration_max.event.hits.hits[0]._source)[
getMetricField(ExecutionMetric.searchDurationMax)
]
: undefined;
const indexingDuration = bucket.indexing_duration_max.event.hits.hits[0]
? parseRuleExecutionLog(bucket.indexing_duration_max.event.hits.hits[0]._source)[
getMetricField(ExecutionMetric.indexingDurationMax)
]
: undefined;
const alertId = logEntry['rule.id'];
const statusDate = logEntry[TIMESTAMP];
const lastFailureAt = lastFailure?.[TIMESTAMP];
const lastFailureMessage = lastFailure?.[MESSAGE];
const lastSuccessAt = lastSuccess?.[TIMESTAMP];
const lastSuccessMessage = lastSuccess?.[MESSAGE];
const status = (logEntry[RULE_STATUS] as RuleExecutionStatus) || null;
const lastLookBackDate = lookBack?.[getMetricField(ExecutionMetric.indexingLookback)];
const gap = executionGap ? moment.duration(executionGap).humanize() : null;
const bulkCreateTimeDurations = indexingDuration
? [makeFloatString(indexingDuration)]
: null;
const searchAfterTimeDurations = searchDuration
? [makeFloatString(searchDuration)]
: null;
return {
alertId,
statusDate,
lastFailureAt,
lastFailureMessage,
lastSuccessAt,
lastSuccessMessage,
status,
lastLookBackDate,
gap,
bulkCreateTimeDurations,
searchAfterTimeDurations,
};
}),
])
);
}
public async logExecutionMetric<T extends ExecutionMetric>({
ruleId,
namespace,
metric,
value,
spaceId,
}: ExecutionMetricArgs<T>) {
await this.create(
{
[SPACE_IDS]: [spaceId],
[EVENT_ACTION]: metric,
[EVENT_KIND]: 'metric',
[getMetricField(metric)]: value,
[RULE_ID]: ruleId,
[TIMESTAMP]: new Date().toISOString(),
},
namespace
);
}
public async logStatusChange({
ruleId,
newStatus,
namespace,
message,
spaceId,
}: LogStatusChangeArgs) {
await this.create(
{
[SPACE_IDS]: [spaceId],
[EVENT_ACTION]: 'status-change',
[EVENT_KIND]: 'event',
[EVENT_SEQUENCE]: this.sequence++,
[MESSAGE]: message,
[RULE_ID]: ruleId,
[RULE_STATUS_SEVERITY]: statusSeverityDict[newStatus],
[RULE_STATUS]: newStatus,
[TIMESTAMP]: new Date().toISOString(),
},
namespace
);
}
public async create(event: RuleExecutionEvent, namespace?: string) {
await this.ruleDataClient.getWriter({ namespace }).bulk({
body: [{ index: {} }, event],
});
}
}

View file

@ -0,0 +1,76 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SearchSort } from '@elastic/elasticsearch/api/types';
import { EVENT_ACTION, TIMESTAMP } from '@kbn/rule-data-utils';
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
import { ExecutionMetric } from '../types';
import { RULE_STATUS, EVENT_SEQUENCE, EVENT_DURATION, EVENT_END } from './constants';
const METRIC_FIELDS = {
[ExecutionMetric.executionGap]: EVENT_DURATION,
[ExecutionMetric.searchDurationMax]: EVENT_DURATION,
[ExecutionMetric.indexingDurationMax]: EVENT_DURATION,
[ExecutionMetric.indexingLookback]: EVENT_END,
};
/**
* Returns ECS field in which metric value is stored
* @deprecated getMetricField is kept here only as a reference. It will be superseded with EventLog implementation
*
* @param metric - execution metric
* @returns ECS field
*/
export const getMetricField = <T extends ExecutionMetric>(metric: T) => METRIC_FIELDS[metric];
/**
* @deprecated sortByTimeDesc is kept here only as a reference. It will be superseded with EventLog implementation
*/
export const sortByTimeDesc: SearchSort = [{ [TIMESTAMP]: 'desc' }, { [EVENT_SEQUENCE]: 'desc' }];
/**
* Builds aggregation to retrieve the most recent metric value
* @deprecated getMetricAggregation is kept here only as a reference. It will be superseded with EventLog implementation
*
* @param metric - execution metric
* @returns aggregation
*/
export const getMetricAggregation = (metric: ExecutionMetric) => ({
filter: {
term: { [EVENT_ACTION]: metric },
},
aggs: {
event: {
top_hits: {
size: 1,
sort: sortByTimeDesc,
_source: [TIMESTAMP, getMetricField(metric)],
},
},
},
});
/**
* Builds aggregation to retrieve the most recent log entry with the given status
* @deprecated getLastEntryAggregation is kept here only as a reference. It will be superseded with EventLog implementation
*
* @param status - rule execution status
* @returns aggregation
*/
export const getLastEntryAggregation = (status: RuleExecutionStatus) => ({
filter: {
term: { [RULE_STATUS]: status },
},
aggs: {
event: {
top_hits: {
sort: sortByTimeDesc,
size: 1,
},
},
},
});

View file

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObjectsFindResult } from '../../../../../../../src/core/server';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
import { IRuleStatusSOAttributes } from '../rules/types';
export enum ExecutionMetric {
'executionGap' = 'executionGap',
'searchDurationMax' = 'searchDurationMax',
'indexingDurationMax' = 'indexingDurationMax',
'indexingLookback' = 'indexingLookback',
}
export type ExecutionMetricValue<T extends ExecutionMetric> = {
[ExecutionMetric.executionGap]: number;
[ExecutionMetric.searchDurationMax]: number;
[ExecutionMetric.indexingDurationMax]: number;
[ExecutionMetric.indexingLookback]: Date;
}[T];
export interface FindExecutionLogArgs {
ruleId: string;
spaceId: string;
logsCount?: number;
}
export interface FindBulkExecutionLogArgs {
ruleIds: string[];
spaceId: string;
logsCount?: number;
}
export interface LogStatusChangeArgs {
ruleId: string;
spaceId: string;
newStatus: RuleExecutionStatus;
namespace?: string;
message?: string;
}
export interface ExecutionMetricArgs<T extends ExecutionMetric> {
ruleId: string;
spaceId: string;
namespace?: string;
metric: T;
value: ExecutionMetricValue<T>;
}
export interface FindBulkExecutionLogResponse {
[ruleId: string]: IRuleStatusSOAttributes[] | undefined;
}
export interface IRuleExecutionLogClient {
find: (
args: FindExecutionLogArgs
) => Promise<Array<SavedObjectsFindResult<IRuleStatusSOAttributes>>>;
findBulk: (args: FindBulkExecutionLogArgs) => Promise<FindBulkExecutionLogResponse>;
create: (event: IRuleStatusSOAttributes, spaceId: string) => Promise<void>;
update: (id: string, event: IRuleStatusSOAttributes, spaceId: string) => 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

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

@ -7,25 +7,25 @@
import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks';
import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { rulesClientMock } from '../../../../../alerting/server/mocks';
import { ruleStatusSavedObjectsClientMock } from '../signals/__mocks__/rule_status_saved_objects_client.mock';
import { deleteRules } from './delete_rules'; import { deleteRules } from './delete_rules';
import { deleteNotifications } from '../notifications/delete_notifications'; import { deleteNotifications } from '../notifications/delete_notifications';
import { deleteRuleActionsSavedObject } from '../rule_actions/delete_rule_actions_saved_object'; import { deleteRuleActionsSavedObject } from '../rule_actions/delete_rule_actions_saved_object';
import { SavedObjectsFindResult } from '../../../../../../../src/core/server'; import { SavedObjectsFindResult } from '../../../../../../../src/core/server';
import { IRuleStatusSOAttributes } from './types'; import { IRuleStatusSOAttributes } from './types';
import { RuleExecutionLogClient } from '../rule_execution_log/__mocks__/rule_execution_log_client';
jest.mock('../notifications/delete_notifications'); jest.mock('../notifications/delete_notifications');
jest.mock('../rule_actions/delete_rule_actions_saved_object'); jest.mock('../rule_actions/delete_rule_actions_saved_object');
describe('deleteRules', () => { describe('deleteRules', () => {
let rulesClient: ReturnType<typeof rulesClientMock.create>; let rulesClient: ReturnType<typeof rulesClientMock.create>;
let ruleStatusClient: ReturnType<typeof ruleStatusSavedObjectsClientMock.create>; let ruleStatusClient: ReturnType<typeof RuleExecutionLogClient>;
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>; let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
beforeEach(() => { beforeEach(() => {
rulesClient = rulesClientMock.create(); rulesClient = rulesClientMock.create();
savedObjectsClient = savedObjectsClientMock.create(); savedObjectsClient = savedObjectsClientMock.create();
ruleStatusClient = ruleStatusSavedObjectsClientMock.create(); ruleStatusClient = new RuleExecutionLogClient();
}); });
it('should delete the rule along with its notifications, actions, and statuses', async () => { it('should delete the rule along with its notifications, actions, and statuses', async () => {
@ -54,12 +54,7 @@ describe('deleteRules', () => {
savedObjectsClient, savedObjectsClient,
ruleStatusClient, ruleStatusClient,
id: 'ruleId', id: 'ruleId',
ruleStatuses: { ruleStatuses: [ruleStatus],
total: 0,
per_page: 0,
page: 0,
saved_objects: [ruleStatus],
},
}; };
await deleteRules(rule); await deleteRules(rule);

View file

@ -19,5 +19,5 @@ export const deleteRules = async ({
await rulesClient.delete({ id }); await rulesClient.delete({ id });
await deleteNotifications({ rulesClient, ruleAlertId: id }); await deleteNotifications({ rulesClient, ruleAlertId: id });
await deleteRuleActionsSavedObject({ ruleAlertId: id, savedObjectsClient }); await deleteRuleActionsSavedObject({ ruleAlertId: id, savedObjectsClient });
ruleStatuses.saved_objects.forEach(async (obj) => ruleStatusClient.delete(obj.id)); ruleStatuses.forEach(async (obj) => ruleStatusClient.delete(obj.id));
}; };

View file

@ -5,16 +5,17 @@
* 2.0. * 2.0.
*/ */
import { SavedObjectsClientContract } from 'kibana/server';
import { SanitizedAlert } from '../../../../../alerting/common'; import { SanitizedAlert } from '../../../../../alerting/common';
import { RulesClient } from '../../../../../alerting/server'; import { RulesClient } from '../../../../../alerting/server';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
import { IRuleExecutionLogClient } from '../rule_execution_log/types';
import { RuleParams } from '../schemas/rule_schemas'; import { RuleParams } from '../schemas/rule_schemas';
import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client';
interface EnableRuleArgs { interface EnableRuleArgs {
rule: SanitizedAlert<RuleParams>; rule: SanitizedAlert<RuleParams>;
rulesClient: RulesClient; rulesClient: RulesClient;
savedObjectsClient: SavedObjectsClientContract; ruleStatusClient: IRuleExecutionLogClient;
spaceId: string;
} }
/** /**
@ -22,26 +23,32 @@ interface EnableRuleArgs {
* *
* @param rule - rule to enable * @param rule - rule to enable
* @param rulesClient - Alerts client * @param rulesClient - Alerts client
* @param savedObjectsClient - Saved Objects client * @param ruleStatusClient - ExecLog client
*/ */
export const enableRule = async ({ rule, rulesClient, savedObjectsClient }: EnableRuleArgs) => { export const enableRule = async ({
rule,
rulesClient,
ruleStatusClient,
spaceId,
}: EnableRuleArgs) => {
await rulesClient.enable({ id: rule.id }); await rulesClient.enable({ id: rule.id });
const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
const ruleCurrentStatus = await ruleStatusClient.find({ const ruleCurrentStatus = await ruleStatusClient.find({
perPage: 1, logsCount: 1,
sortField: 'statusDate', ruleId: rule.id,
sortOrder: 'desc', spaceId,
search: rule.id,
searchFields: ['alertId'],
}); });
// set current status for this rule to be 'going to run' // set current status for this rule to be 'going to run'
if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { if (ruleCurrentStatus && ruleCurrentStatus.length > 0) {
const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; const currentStatusToDisable = ruleCurrentStatus[0];
await ruleStatusClient.update(currentStatusToDisable.id, { await ruleStatusClient.update(
currentStatusToDisable.id,
{
...currentStatusToDisable.attributes, ...currentStatusToDisable.attributes,
status: 'going to run', status: RuleExecutionStatus['going to run'],
}); },
spaceId
);
} }
}; };

View file

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

View file

@ -31,7 +31,8 @@ export const patchRules = async ({
rulesClient, rulesClient,
author, author,
buildingBlockType, buildingBlockType,
savedObjectsClient, ruleStatusClient,
spaceId,
description, description,
eventCategoryOverride, eventCategoryOverride,
falsePositives, falsePositives,
@ -200,7 +201,7 @@ export const patchRules = async ({
if (rule.enabled && enabled === false) { if (rule.enabled && enabled === false) {
await rulesClient.disable({ id: rule.id }); await rulesClient.disable({ id: rule.id });
} else if (!rule.enabled && enabled === true) { } else if (!rule.enabled && enabled === true) {
await enableRule({ rule, rulesClient, savedObjectsClient }); await enableRule({ rule, rulesClient, ruleStatusClient, spaceId });
} else { } else {
// enabled is null or undefined and we do not touch the rule // enabled is null or undefined and we do not touch the rule
} }

View file

@ -13,6 +13,7 @@ import {
SavedObjectAttributes, SavedObjectAttributes,
SavedObjectsFindResponse, SavedObjectsFindResponse,
SavedObjectsClientContract, SavedObjectsClientContract,
SavedObjectsFindResult,
} from 'kibana/server'; } from 'kibana/server';
import type { import type {
MachineLearningJobIdOrUndefined, MachineLearningJobIdOrUndefined,
@ -86,7 +87,7 @@ import {
QueryFilterOrUndefined, QueryFilterOrUndefined,
FieldsOrUndefined, FieldsOrUndefined,
SortOrderOrUndefined, SortOrderOrUndefined,
JobStatus, RuleExecutionStatus,
LastSuccessAt, LastSuccessAt,
StatusDate, StatusDate,
LastSuccessMessage, LastSuccessMessage,
@ -106,7 +107,7 @@ import { Alert, SanitizedAlert } from '../../../../../alerting/common';
import { SIGNALS_ID } from '../../../../common/constants'; import { SIGNALS_ID } from '../../../../common/constants';
import { PartialFilter } from '../types'; import { PartialFilter } from '../types';
import { RuleParams } from '../schemas/rule_schemas'; import { RuleParams } from '../schemas/rule_schemas';
import { RuleStatusSavedObjectsClient } from '../signals/rule_status_saved_objects_client'; import { IRuleExecutionLogClient } from '../rule_execution_log/types';
export type RuleAlertType = Alert<RuleParams>; export type RuleAlertType = Alert<RuleParams>;
@ -118,7 +119,7 @@ export interface IRuleStatusSOAttributes extends Record<string, any> {
lastFailureMessage: LastFailureMessage | null | undefined; lastFailureMessage: LastFailureMessage | null | undefined;
lastSuccessAt: LastSuccessAt | null | undefined; lastSuccessAt: LastSuccessAt | null | undefined;
lastSuccessMessage: LastSuccessMessage | null | undefined; lastSuccessMessage: LastSuccessMessage | null | undefined;
status: JobStatus | null | undefined; status: RuleExecutionStatus | null | undefined;
lastLookBackDate: string | null | undefined; lastLookBackDate: string | null | undefined;
gap: string | null | undefined; gap: string | null | undefined;
bulkCreateTimeDurations: string[] | null | undefined; bulkCreateTimeDurations: string[] | null | undefined;
@ -132,7 +133,7 @@ export interface IRuleStatusResponseAttributes {
last_failure_message: LastFailureMessage | null | undefined; last_failure_message: LastFailureMessage | null | undefined;
last_success_at: LastSuccessAt | null | undefined; last_success_at: LastSuccessAt | null | undefined;
last_success_message: LastSuccessMessage | null | undefined; last_success_message: LastSuccessMessage | null | undefined;
status: JobStatus | null | undefined; status: RuleExecutionStatus | null | undefined;
last_look_back_date: string | null | undefined; // NOTE: This is no longer used on the UI, but left here in case users are using it within the API last_look_back_date: string | null | undefined; // NOTE: This is no longer used on the UI, but left here in case users are using it within the API
gap: string | null | undefined; gap: string | null | undefined;
bulk_create_time_durations: string[] | null | undefined; bulk_create_time_durations: string[] | null | undefined;
@ -266,14 +267,16 @@ export interface CreateRulesOptions {
} }
export interface UpdateRulesOptions { export interface UpdateRulesOptions {
savedObjectsClient: SavedObjectsClientContract; spaceId: string;
ruleStatusClient: IRuleExecutionLogClient;
rulesClient: RulesClient; rulesClient: RulesClient;
defaultOutputIndex: string; defaultOutputIndex: string;
ruleUpdate: UpdateRulesSchema; ruleUpdate: UpdateRulesSchema;
} }
export interface PatchRulesOptions { export interface PatchRulesOptions {
savedObjectsClient: SavedObjectsClientContract; spaceId: string;
ruleStatusClient: IRuleExecutionLogClient;
rulesClient: RulesClient; rulesClient: RulesClient;
anomalyThreshold: AnomalyThresholdOrUndefined; anomalyThreshold: AnomalyThresholdOrUndefined;
author: AuthorOrUndefined; author: AuthorOrUndefined;
@ -332,8 +335,8 @@ export interface ReadRuleOptions {
export interface DeleteRuleOptions { export interface DeleteRuleOptions {
rulesClient: RulesClient; rulesClient: RulesClient;
savedObjectsClient: SavedObjectsClientContract; savedObjectsClient: SavedObjectsClientContract;
ruleStatusClient: RuleStatusSavedObjectsClient; ruleStatusClient: IRuleExecutionLogClient;
ruleStatuses: SavedObjectsFindResponse<IRuleStatusSOAttributes, unknown>; ruleStatuses: Array<SavedObjectsFindResult<IRuleStatusSOAttributes>>;
id: Id; id: Id;
} }

View file

@ -5,21 +5,21 @@
* 2.0. * 2.0.
*/ */
import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks';
import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { rulesClientMock } from '../../../../../alerting/server/mocks';
import { getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; import { getFindResultWithSingleHit } from '../routes/__mocks__/request_responses';
import { updatePrepackagedRules } from './update_prepacked_rules'; import { updatePrepackagedRules } from './update_prepacked_rules';
import { patchRules } from './patch_rules'; import { patchRules } from './patch_rules';
import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock'; import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock';
import { RuleExecutionLogClient } from '../rule_execution_log/__mocks__/rule_execution_log_client';
jest.mock('./patch_rules'); jest.mock('./patch_rules');
describe('updatePrepackagedRules', () => { describe('updatePrepackagedRules', () => {
let rulesClient: ReturnType<typeof rulesClientMock.create>; let rulesClient: ReturnType<typeof rulesClientMock.create>;
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>; let ruleStatusClient: ReturnType<typeof RuleExecutionLogClient>;
beforeEach(() => { beforeEach(() => {
rulesClient = rulesClientMock.create(); rulesClient = rulesClientMock.create();
savedObjectsClient = savedObjectsClientMock.create(); ruleStatusClient = new RuleExecutionLogClient();
}); });
it('should omit actions and enabled when calling patchRules', async () => { it('should omit actions and enabled when calling patchRules', async () => {
@ -37,7 +37,8 @@ describe('updatePrepackagedRules', () => {
await updatePrepackagedRules( await updatePrepackagedRules(
rulesClient, rulesClient,
savedObjectsClient, 'default',
ruleStatusClient,
[{ ...prepackagedRule, actions }], [{ ...prepackagedRule, actions }],
outputIndex outputIndex
); );

View file

@ -5,7 +5,6 @@
* 2.0. * 2.0.
*/ */
import { SavedObjectsClientContract } from 'kibana/server';
import { chunk } from 'lodash/fp'; import { chunk } from 'lodash/fp';
import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema';
import { RulesClient, PartialAlert } from '../../../../../alerting/server'; import { RulesClient, PartialAlert } from '../../../../../alerting/server';
@ -13,6 +12,7 @@ import { patchRules } from './patch_rules';
import { readRules } from './read_rules'; import { readRules } from './read_rules';
import { PartialFilter } from '../types'; import { PartialFilter } from '../types';
import { RuleParams } from '../schemas/rule_schemas'; import { RuleParams } from '../schemas/rule_schemas';
import { IRuleExecutionLogClient } from '../rule_execution_log/types';
/** /**
* How many rules to update at a time is set to 50 from errors coming from * How many rules to update at a time is set to 50 from errors coming from
@ -44,19 +44,27 @@ export const UPDATE_CHUNK_SIZE = 50;
* This implements a chunked approach to not saturate network connections and * This implements a chunked approach to not saturate network connections and
* avoid being a "noisy neighbor". * avoid being a "noisy neighbor".
* @param rulesClient Alerting client * @param rulesClient Alerting client
* @param savedObjectsClient Saved object client * @param spaceId Current user spaceId
* @param ruleStatusClient Rule execution log client
* @param rules The rules to apply the update for * @param rules The rules to apply the update for
* @param outputIndex The output index to apply the update to. * @param outputIndex The output index to apply the update to.
*/ */
export const updatePrepackagedRules = async ( export const updatePrepackagedRules = async (
rulesClient: RulesClient, rulesClient: RulesClient,
savedObjectsClient: SavedObjectsClientContract, spaceId: string,
ruleStatusClient: IRuleExecutionLogClient,
rules: AddPrepackagedRulesSchemaDecoded[], rules: AddPrepackagedRulesSchemaDecoded[],
outputIndex: string outputIndex: string
): Promise<void> => { ): Promise<void> => {
const ruleChunks = chunk(UPDATE_CHUNK_SIZE, rules); const ruleChunks = chunk(UPDATE_CHUNK_SIZE, rules);
for (const ruleChunk of ruleChunks) { for (const ruleChunk of ruleChunks) {
const rulePromises = createPromises(rulesClient, savedObjectsClient, ruleChunk, outputIndex); const rulePromises = createPromises(
rulesClient,
spaceId,
ruleStatusClient,
ruleChunk,
outputIndex
);
await Promise.all(rulePromises); await Promise.all(rulePromises);
} }
}; };
@ -64,14 +72,16 @@ export const updatePrepackagedRules = async (
/** /**
* Creates promises of the rules and returns them. * Creates promises of the rules and returns them.
* @param rulesClient Alerting client * @param rulesClient Alerting client
* @param savedObjectsClient Saved object client * @param spaceId Current user spaceId
* @param ruleStatusClient Rule execution log client
* @param rules The rules to apply the update for * @param rules The rules to apply the update for
* @param outputIndex The output index to apply the update to. * @param outputIndex The output index to apply the update to.
* @returns Promise of what was updated. * @returns Promise of what was updated.
*/ */
export const createPromises = ( export const createPromises = (
rulesClient: RulesClient, rulesClient: RulesClient,
savedObjectsClient: SavedObjectsClientContract, spaceId: string,
ruleStatusClient: IRuleExecutionLogClient,
rules: AddPrepackagedRulesSchemaDecoded[], rules: AddPrepackagedRulesSchemaDecoded[],
outputIndex: string outputIndex: string
): Array<Promise<PartialAlert<RuleParams> | null>> => { ): Array<Promise<PartialAlert<RuleParams> | null>> => {
@ -143,7 +153,8 @@ export const createPromises = (
outputIndex, outputIndex,
rule: existingRule, rule: existingRule,
savedId, savedId,
savedObjectsClient, spaceId,
ruleStatusClient,
meta, meta,
filters, filters,
index, index,

View file

@ -5,24 +5,26 @@
* 2.0. * 2.0.
*/ */
import { UpdateRulesOptions } from './types';
import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { rulesClientMock } from '../../../../../alerting/server/mocks';
import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks';
import { import {
getUpdateRulesSchemaMock,
getUpdateMachineLearningSchemaMock, getUpdateMachineLearningSchemaMock,
getUpdateRulesSchemaMock,
} from '../../../../common/detection_engine/schemas/request/rule_schemas.mock'; } from '../../../../common/detection_engine/schemas/request/rule_schemas.mock';
import { RuleExecutionLogClient } from '../rule_execution_log/__mocks__/rule_execution_log_client';
import { UpdateRulesOptions } from './types';
export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({ export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({
spaceId: 'default',
rulesClient: rulesClientMock.create(), rulesClient: rulesClientMock.create(),
savedObjectsClient: savedObjectsClientMock.create(), ruleStatusClient: new RuleExecutionLogClient(),
defaultOutputIndex: '.siem-signals-default', defaultOutputIndex: '.siem-signals-default',
ruleUpdate: getUpdateRulesSchemaMock(), ruleUpdate: getUpdateRulesSchemaMock(),
}); });
export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({ export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({
spaceId: 'default',
rulesClient: rulesClientMock.create(), rulesClient: rulesClientMock.create(),
savedObjectsClient: savedObjectsClientMock.create(), ruleStatusClient: new RuleExecutionLogClient(),
defaultOutputIndex: '.siem-signals-default', defaultOutputIndex: '.siem-signals-default',
ruleUpdate: getUpdateMachineLearningSchemaMock(), ruleUpdate: getUpdateMachineLearningSchemaMock(),
}); });

View file

@ -18,8 +18,9 @@ import { InternalRuleUpdate, RuleParams } from '../schemas/rule_schemas';
import { enableRule } from './enable_rule'; import { enableRule } from './enable_rule';
export const updateRules = async ({ export const updateRules = async ({
spaceId,
rulesClient, rulesClient,
savedObjectsClient, ruleStatusClient,
defaultOutputIndex, defaultOutputIndex,
ruleUpdate, ruleUpdate,
}: UpdateRulesOptions): Promise<PartialAlert<RuleParams> | null> => { }: UpdateRulesOptions): Promise<PartialAlert<RuleParams> | null> => {
@ -88,7 +89,7 @@ export const updateRules = async ({
if (existingRule.enabled && enabled === false) { if (existingRule.enabled && enabled === false) {
await rulesClient.disable({ id: existingRule.id }); await rulesClient.disable({ id: existingRule.id });
} else if (!existingRule.enabled && enabled === true) { } else if (!existingRule.enabled && enabled === true) {
await enableRule({ rule: existingRule, rulesClient, savedObjectsClient }); await enableRule({ rule: existingRule, rulesClient, ruleStatusClient, spaceId });
} }
return { ...update, enabled }; return { ...update, enabled };
}; };

View file

@ -31,6 +31,7 @@ import { transformRuleToAlertAction } from '../../../../common/detection_engine/
import { SanitizedAlert } from '../../../../../alerting/common'; import { SanitizedAlert } from '../../../../../alerting/common';
import { IRuleStatusSOAttributes } from '../rules/types'; import { IRuleStatusSOAttributes } from '../rules/types';
import { transformTags } from '../routes/rules/utils'; import { transformTags } from '../routes/rules/utils';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
// These functions provide conversions from the request API schema to the internal rule schema and from the internal rule schema // These functions provide conversions from the request API schema to the internal rule schema and from the internal rule schema
// to the response API schema. This provides static type-check assurances that the internal schema is in sync with the API schema for // to the response API schema. This provides static type-check assurances that the internal schema is in sync with the API schema for
@ -315,7 +316,7 @@ export const mergeAlertWithSidecarStatus = (
lastFailureMessage: `Reason: ${alert.executionStatus.error?.reason} Message: ${alert.executionStatus.error?.message}`, lastFailureMessage: `Reason: ${alert.executionStatus.error?.reason} Message: ${alert.executionStatus.error?.message}`,
lastFailureAt: alert.executionStatus.lastExecutionDate.toISOString(), lastFailureAt: alert.executionStatus.lastExecutionDate.toISOString(),
statusDate: alert.executionStatus.lastExecutionDate.toISOString(), statusDate: alert.executionStatus.lastExecutionDate.toISOString(),
status: 'failed', status: RuleExecutionStatus.failed,
}; };
} }
return status; return status;

View file

@ -15,7 +15,7 @@ import type {
WrappedSignalHit, WrappedSignalHit,
AlertAttributes, AlertAttributes,
} from '../types'; } from '../types';
import { SavedObject, SavedObjectsFindResponse } from '../../../../../../../../src/core/server'; import { SavedObject, SavedObjectsFindResult } from '../../../../../../../../src/core/server';
import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks'; import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks';
import { IRuleStatusSOAttributes } from '../../rules/types'; import { IRuleStatusSOAttributes } from '../../rules/types';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
@ -23,6 +23,7 @@ import { getListArrayMock } from '../../../../../common/detection_engine/schemas
import { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response';
import { RuleParams } from '../../schemas/rule_schemas'; import { RuleParams } from '../../schemas/rule_schemas';
import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock';
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
export const sampleRuleSO = <T extends RuleParams>(params: T): SavedObject<AlertAttributes<T>> => { export const sampleRuleSO = <T extends RuleParams>(params: T): SavedObject<AlertAttributes<T>> => {
return { return {
@ -326,7 +327,7 @@ export const sampleSignalHit = (): SignalHit => ({
type: 'query', type: 'query',
threat: [], threat: [],
version: 1, version: 1,
status: 'succeeded', status: RuleExecutionStatus.succeeded,
status_date: '2020-02-22T16:47:50.047Z', status_date: '2020-02-22T16:47:50.047Z',
last_success_at: '2020-02-22T16:47:50.047Z', last_success_at: '2020-02-22T16:47:50.047Z',
last_success_message: 'succeeded', last_success_message: 'succeeded',
@ -391,7 +392,7 @@ export const sampleThresholdSignalHit = (): SignalHit => ({
type: 'query', type: 'query',
threat: [], threat: [],
version: 1, version: 1,
status: 'succeeded', status: RuleExecutionStatus.succeeded,
status_date: '2020-02-22T16:47:50.047Z', status_date: '2020-02-22T16:47:50.047Z',
last_success_at: '2020-02-22T16:47:50.047Z', last_success_at: '2020-02-22T16:47:50.047Z',
last_success_message: 'succeeded', last_success_message: 'succeeded',
@ -712,7 +713,7 @@ export const exampleRuleStatus: () => SavedObject<IRuleStatusSOAttributes> = ()
attributes: { attributes: {
alertId: 'f4b8e31d-cf93-4bde-a265-298bde885cd7', alertId: 'f4b8e31d-cf93-4bde-a265-298bde885cd7',
statusDate: '2020-03-27T22:55:59.517Z', statusDate: '2020-03-27T22:55:59.517Z',
status: 'succeeded', status: RuleExecutionStatus.succeeded,
lastFailureAt: null, lastFailureAt: null,
lastSuccessAt: '2020-03-27T22:55:59.517Z', lastSuccessAt: '2020-03-27T22:55:59.517Z',
lastFailureMessage: null, lastFailureMessage: null,
@ -729,14 +730,9 @@ export const exampleRuleStatus: () => SavedObject<IRuleStatusSOAttributes> = ()
export const exampleFindRuleStatusResponse: ( export const exampleFindRuleStatusResponse: (
mockStatuses: Array<SavedObject<IRuleStatusSOAttributes>> mockStatuses: Array<SavedObject<IRuleStatusSOAttributes>>
) => SavedObjectsFindResponse<IRuleStatusSOAttributes> = ( ) => Array<SavedObjectsFindResult<IRuleStatusSOAttributes>> = (
mockStatuses = [exampleRuleStatus()] mockStatuses = [exampleRuleStatus()]
) => ({ ) => mockStatuses.map((obj) => ({ ...obj, score: 1 }));
total: 1,
per_page: 6,
page: 1,
saved_objects: mockStatuses.map((obj) => ({ ...obj, score: 1 })),
});
export const mockLogger = loggingSystemMock.createLogger(); export const mockLogger = loggingSystemMock.createLogger();

View file

@ -20,6 +20,7 @@ import {
} from '../../../../common/detection_engine/schemas/response/rules_schema.mocks'; } from '../../../../common/detection_engine/schemas/response/rules_schema.mocks';
import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock';
import { SIGNALS_TEMPLATE_VERSION } from '../routes/index/get_signals_template'; import { SIGNALS_TEMPLATE_VERSION } from '../routes/index/get_signals_template';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
describe('buildSignal', () => { describe('buildSignal', () => {
beforeEach(() => { beforeEach(() => {
@ -84,7 +85,7 @@ describe('buildSignal', () => {
type: 'query', type: 'query',
threat: [], threat: [],
version: 1, version: 1,
status: 'succeeded', status: RuleExecutionStatus.succeeded,
status_date: '2020-02-22T16:47:50.047Z', status_date: '2020-02-22T16:47:50.047Z',
last_success_at: '2020-02-22T16:47:50.047Z', last_success_at: '2020-02-22T16:47:50.047Z',
last_success_message: 'succeeded', last_success_message: 'succeeded',
@ -171,7 +172,7 @@ describe('buildSignal', () => {
type: 'query', type: 'query',
threat: [], threat: [],
version: 1, version: 1,
status: 'succeeded', status: RuleExecutionStatus.succeeded,
status_date: '2020-02-22T16:47:50.047Z', status_date: '2020-02-22T16:47:50.047Z',
last_success_at: '2020-02-22T16:47:50.047Z', last_success_at: '2020-02-22T16:47:50.047Z',
last_success_message: 'succeeded', last_success_message: 'succeeded',

View file

@ -10,6 +10,7 @@ import { SavedObject } from 'src/core/server';
import { IRuleStatusSOAttributes } from '../rules/types'; import { IRuleStatusSOAttributes } from '../rules/types';
import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client';
import { getRuleStatusSavedObjects } from './get_rule_status_saved_objects'; import { getRuleStatusSavedObjects } from './get_rule_status_saved_objects';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
interface RuleStatusParams { interface RuleStatusParams {
alertId: string; alertId: string;
@ -24,7 +25,7 @@ export const createNewRuleStatus = async ({
return ruleStatusClient.create({ return ruleStatusClient.create({
alertId, alertId,
statusDate: now, statusDate: now,
status: 'going to run', status: RuleExecutionStatus['going to run'],
lastFailureAt: null, lastFailureAt: null,
lastSuccessAt: null, lastSuccessAt: null,
lastFailureMessage: null, lastFailureMessage: null,
@ -44,8 +45,8 @@ export const getOrCreateRuleStatuses = async ({
alertId, alertId,
ruleStatusClient, ruleStatusClient,
}); });
if (ruleStatuses.saved_objects.length > 0) { if (ruleStatuses.length > 0) {
return ruleStatuses.saved_objects; return ruleStatuses;
} }
const newStatus = await createNewRuleStatus({ alertId, ruleStatusClient }); const newStatus = await createNewRuleStatus({ alertId, ruleStatusClient });

View file

@ -5,7 +5,7 @@
* 2.0. * 2.0.
*/ */
import { SavedObjectsFindResponse } from 'kibana/server'; import { SavedObjectsFindResult } from 'kibana/server';
import { IRuleStatusSOAttributes } from '../rules/types'; import { IRuleStatusSOAttributes } from '../rules/types';
import { MAX_RULE_STATUSES } from './rule_status_service'; import { MAX_RULE_STATUSES } from './rule_status_service';
import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client';
@ -18,7 +18,7 @@ interface GetRuleStatusSavedObject {
export const getRuleStatusSavedObjects = async ({ export const getRuleStatusSavedObjects = async ({
alertId, alertId,
ruleStatusClient, ruleStatusClient,
}: GetRuleStatusSavedObject): Promise<SavedObjectsFindResponse<IRuleStatusSOAttributes>> => { }: GetRuleStatusSavedObject): Promise<Array<SavedObjectsFindResult<IRuleStatusSOAttributes>>> => {
return ruleStatusClient.find({ return ruleStatusClient.find({
perPage: MAX_RULE_STATUSES, perPage: MAX_RULE_STATUSES,
sortField: 'statusDate', sortField: 'statusDate',

View file

@ -11,7 +11,7 @@ import {
SavedObject, SavedObject,
SavedObjectsUpdateResponse, SavedObjectsUpdateResponse,
SavedObjectsFindOptions, SavedObjectsFindOptions,
SavedObjectsFindResponse, SavedObjectsFindResult,
} from '../../../../../../../src/core/server'; } from '../../../../../../../src/core/server';
import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings';
import { IRuleStatusSOAttributes } from '../rules/types'; import { IRuleStatusSOAttributes } from '../rules/types';
@ -20,7 +20,7 @@ import { buildChunkedOrFilter } from './utils';
export interface RuleStatusSavedObjectsClient { export interface RuleStatusSavedObjectsClient {
find: ( find: (
options?: Omit<SavedObjectsFindOptions, 'type'> options?: Omit<SavedObjectsFindOptions, 'type'>
) => Promise<SavedObjectsFindResponse<IRuleStatusSOAttributes>>; ) => Promise<Array<SavedObjectsFindResult<IRuleStatusSOAttributes>>>;
findBulk: (ids: string[], statusesPerId: number) => Promise<FindBulkResponse>; findBulk: (ids: string[], statusesPerId: number) => Promise<FindBulkResponse>;
create: (attributes: IRuleStatusSOAttributes) => Promise<SavedObject<IRuleStatusSOAttributes>>; create: (attributes: IRuleStatusSOAttributes) => Promise<SavedObject<IRuleStatusSOAttributes>>;
update: ( update: (
@ -30,18 +30,20 @@ export interface RuleStatusSavedObjectsClient {
delete: (id: string) => Promise<{}>; delete: (id: string) => Promise<{}>;
} }
interface FindBulkResponse { export interface FindBulkResponse {
[key: string]: IRuleStatusSOAttributes[] | undefined; [key: string]: IRuleStatusSOAttributes[] | undefined;
} }
export const ruleStatusSavedObjectsClientFactory = ( export const ruleStatusSavedObjectsClientFactory = (
savedObjectsClient: SavedObjectsClientContract savedObjectsClient: SavedObjectsClientContract
): RuleStatusSavedObjectsClient => ({ ): RuleStatusSavedObjectsClient => ({
find: (options) => find: async (options) => {
savedObjectsClient.find<IRuleStatusSOAttributes>({ const result = await savedObjectsClient.find<IRuleStatusSOAttributes>({
...options, ...options,
type: ruleStatusSavedObjectType, type: ruleStatusSavedObjectType,
}), });
return result.saved_objects;
},
findBulk: async (ids, statusesPerId) => { findBulk: async (ids, statusesPerId) => {
if (ids.length === 0) { if (ids.length === 0) {
return {}; return {};

View file

@ -13,6 +13,7 @@ import {
MAX_RULE_STATUSES, MAX_RULE_STATUSES,
} from './rule_status_service'; } from './rule_status_service';
import { exampleRuleStatus, exampleFindRuleStatusResponse } from './__mocks__/es_results'; import { exampleRuleStatus, exampleFindRuleStatusResponse } from './__mocks__/es_results';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
const expectIsoDateString = expect.stringMatching(/2.*Z$/); const expectIsoDateString = expect.stringMatching(/2.*Z$/);
const buildStatuses = (n: number) => const buildStatuses = (n: number) =>
@ -25,9 +26,11 @@ const buildStatuses = (n: number) =>
describe('buildRuleStatusAttributes', () => { describe('buildRuleStatusAttributes', () => {
it('generates a new date on each call', async () => { it('generates a new date on each call', async () => {
const { statusDate } = buildRuleStatusAttributes('going to run'); const { statusDate } = buildRuleStatusAttributes(RuleExecutionStatus['going to run']);
await new Promise((resolve) => setTimeout(resolve, 10)); // ensure time has passed await new Promise((resolve) => setTimeout(resolve, 10)); // ensure time has passed
const { statusDate: statusDate2 } = buildRuleStatusAttributes('going to run'); const { statusDate: statusDate2 } = buildRuleStatusAttributes(
RuleExecutionStatus['going to run']
);
expect(statusDate).toEqual(expectIsoDateString); expect(statusDate).toEqual(expectIsoDateString);
expect(statusDate2).toEqual(expectIsoDateString); expect(statusDate2).toEqual(expectIsoDateString);
@ -35,7 +38,7 @@ describe('buildRuleStatusAttributes', () => {
}); });
it('returns a status and statusDate if "going to run"', () => { it('returns a status and statusDate if "going to run"', () => {
const result = buildRuleStatusAttributes('going to run'); const result = buildRuleStatusAttributes(RuleExecutionStatus['going to run']);
expect(result).toEqual({ expect(result).toEqual({
status: 'going to run', status: 'going to run',
statusDate: expectIsoDateString, statusDate: expectIsoDateString,
@ -43,7 +46,7 @@ describe('buildRuleStatusAttributes', () => {
}); });
it('returns success fields if "success"', () => { it('returns success fields if "success"', () => {
const result = buildRuleStatusAttributes('succeeded', 'success message'); const result = buildRuleStatusAttributes(RuleExecutionStatus.succeeded, 'success message');
expect(result).toEqual({ expect(result).toEqual({
status: 'succeeded', status: 'succeeded',
statusDate: expectIsoDateString, statusDate: expectIsoDateString,
@ -56,7 +59,7 @@ describe('buildRuleStatusAttributes', () => {
it('returns warning fields if "warning"', () => { it('returns warning fields if "warning"', () => {
const result = buildRuleStatusAttributes( const result = buildRuleStatusAttributes(
'warning', RuleExecutionStatus.warning,
'some indices missing timestamp override field' 'some indices missing timestamp override field'
); );
expect(result).toEqual({ expect(result).toEqual({
@ -70,7 +73,7 @@ describe('buildRuleStatusAttributes', () => {
}); });
it('returns failure fields if "failed"', () => { it('returns failure fields if "failed"', () => {
const result = buildRuleStatusAttributes('failed', 'failure message'); const result = buildRuleStatusAttributes(RuleExecutionStatus.failed, 'failure message');
expect(result).toEqual({ expect(result).toEqual({
status: 'failed', status: 'failed',
statusDate: expectIsoDateString, statusDate: expectIsoDateString,

View file

@ -6,7 +6,7 @@
*/ */
import { assertUnreachable } from '../../../../common/utility_types'; import { assertUnreachable } from '../../../../common/utility_types';
import { JobStatus } from '../../../../common/detection_engine/schemas/common/schemas'; import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
import { IRuleStatusSOAttributes } from '../rules/types'; import { IRuleStatusSOAttributes } from '../rules/types';
import { getOrCreateRuleStatuses } from './get_or_create_rule_statuses'; import { getOrCreateRuleStatuses } from './get_or_create_rule_statuses';
import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client';
@ -29,7 +29,7 @@ export interface RuleStatusService {
} }
export const buildRuleStatusAttributes: ( export const buildRuleStatusAttributes: (
status: JobStatus, status: RuleExecutionStatus,
message?: string, message?: string,
attributes?: Attributes attributes?: Attributes
) => Partial<IRuleStatusSOAttributes> = (status, message, attributes = {}) => { ) => Partial<IRuleStatusSOAttributes> = (status, message, attributes = {}) => {
@ -41,35 +41,35 @@ export const buildRuleStatusAttributes: (
}; };
switch (status) { switch (status) {
case 'succeeded': { case RuleExecutionStatus.succeeded: {
return { return {
...baseAttributes, ...baseAttributes,
lastSuccessAt: now, lastSuccessAt: now,
lastSuccessMessage: message, lastSuccessMessage: message,
}; };
} }
case 'warning': { case RuleExecutionStatus.warning: {
return { return {
...baseAttributes, ...baseAttributes,
lastSuccessAt: now, lastSuccessAt: now,
lastSuccessMessage: message, lastSuccessMessage: message,
}; };
} }
case 'partial failure': { case RuleExecutionStatus['partial failure']: {
return { return {
...baseAttributes, ...baseAttributes,
lastSuccessAt: now, lastSuccessAt: now,
lastSuccessMessage: message, lastSuccessMessage: message,
}; };
} }
case 'failed': { case RuleExecutionStatus.failed: {
return { return {
...baseAttributes, ...baseAttributes,
lastFailureAt: now, lastFailureAt: now,
lastFailureMessage: message, lastFailureMessage: message,
}; };
} }
case 'going to run': { case RuleExecutionStatus['going to run']: {
return baseAttributes; return baseAttributes;
} }
} }
@ -93,7 +93,7 @@ export const ruleStatusServiceFactory = async ({
await ruleStatusClient.update(currentStatus.id, { await ruleStatusClient.update(currentStatus.id, {
...currentStatus.attributes, ...currentStatus.attributes,
...buildRuleStatusAttributes('going to run'), ...buildRuleStatusAttributes(RuleExecutionStatus['going to run']),
}); });
}, },
@ -105,7 +105,7 @@ export const ruleStatusServiceFactory = async ({
await ruleStatusClient.update(currentStatus.id, { await ruleStatusClient.update(currentStatus.id, {
...currentStatus.attributes, ...currentStatus.attributes,
...buildRuleStatusAttributes('succeeded', message, attributes), ...buildRuleStatusAttributes(RuleExecutionStatus.succeeded, message, attributes),
}); });
}, },
@ -117,7 +117,7 @@ export const ruleStatusServiceFactory = async ({
await ruleStatusClient.update(currentStatus.id, { await ruleStatusClient.update(currentStatus.id, {
...currentStatus.attributes, ...currentStatus.attributes,
...buildRuleStatusAttributes('partial failure', message, attributes), ...buildRuleStatusAttributes(RuleExecutionStatus['partial failure'], message, attributes),
}); });
}, },
@ -130,7 +130,7 @@ export const ruleStatusServiceFactory = async ({
const failureAttributes = { const failureAttributes = {
...currentStatus.attributes, ...currentStatus.attributes,
...buildRuleStatusAttributes('failed', message, attributes), ...buildRuleStatusAttributes(RuleExecutionStatus.failed, message, attributes),
}; };
// We always update the newest status, so to 'persist' a failure we push a copy to the head of the list // We always update the newest status, so to 'persist' a failure we push a copy to the head of the list

View file

@ -69,6 +69,7 @@ import {
REFERENCE_RULE_ALERT_TYPE_ID, REFERENCE_RULE_ALERT_TYPE_ID,
REFERENCE_RULE_PERSISTENCE_ALERT_TYPE_ID, REFERENCE_RULE_PERSISTENCE_ALERT_TYPE_ID,
CUSTOM_ALERT_TYPE_ID, CUSTOM_ALERT_TYPE_ID,
DEFAULT_SPACE_ID,
} from '../common/constants'; } from '../common/constants';
import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerEndpointRoutes } from './endpoint/routes/metadata';
import { registerLimitedConcurrencyRoutes } from './endpoint/routes/limited_concurrency'; import { registerLimitedConcurrencyRoutes } from './endpoint/routes/limited_concurrency';
@ -91,6 +92,7 @@ import { licenseService } from './lib/license';
import { PolicyWatcher } from './endpoint/lib/policy/license_watch'; import { PolicyWatcher } from './endpoint/lib/policy/license_watch';
import { parseExperimentalConfigValue } from '../common/experimental_features'; import { parseExperimentalConfigValue } from '../common/experimental_features';
import { migrateArtifactsToFleet } from './endpoint/lib/artifacts/migrate_artifacts_to_fleet'; import { migrateArtifactsToFleet } from './endpoint/lib/artifacts/migrate_artifacts_to_fleet';
import { RuleExecutionLogClient } from './lib/detection_engine/rule_execution_log/rule_execution_log_client';
import { getKibanaPrivilegesFeaturePrivileges } from './features'; import { getKibanaPrivilegesFeaturePrivileges } from './features';
import { EndpointMetadataService } from './endpoint/services/metadata'; import { EndpointMetadataService } from './endpoint/services/metadata';
@ -184,6 +186,13 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
APP_ID, APP_ID,
(context, request, response) => ({ (context, request, response) => ({
getAppClient: () => this.appClientFactory.create(request), getAppClient: () => this.appClientFactory.create(request),
getSpaceId: () => plugins.spaces?.spacesService?.getSpaceId(request) || DEFAULT_SPACE_ID,
getExecutionLogClient: () =>
new RuleExecutionLogClient({
ruleDataService: plugins.ruleRegistry.ruleDataService,
// TODO check if savedObjects.client contains spaceId
savedObjectsClient: context.core.savedObjects.client,
}),
}) })
); );
@ -202,11 +211,10 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
const alertsIndexPattern = ruleDataService.getFullAssetName('security.alerts*'); const alertsIndexPattern = ruleDataService.getFullAssetName('security.alerts*');
const initializeRuleDataTemplates = once(async () => { const initializeRuleDataTemplates = once(async () => {
const componentTemplateName = ruleDataService.getFullAssetName('security.alerts-mappings');
if (!ruleDataService.isWriteEnabled()) { if (!ruleDataService.isWriteEnabled()) {
return; return;
} }
const componentTemplateName = ruleDataService.getFullAssetName('security.alerts-mappings');
await ruleDataService.createOrUpdateComponentTemplate({ await ruleDataService.createOrUpdateComponentTemplate({
name: componentTemplateName, name: componentTemplateName,
@ -245,8 +253,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
() => initializeRuleDataTemplatesPromise () => initializeRuleDataTemplatesPromise
); );
// sec
// Register reference rule types via rule-registry // Register reference rule types via rule-registry
this.setupPlugins.alerting.registerType(createQueryAlertType(ruleDataClient, this.logger)); this.setupPlugins.alerting.registerType(createQueryAlertType(ruleDataClient, this.logger));
this.setupPlugins.alerting.registerType(createEqlAlertType(ruleDataClient, this.logger)); this.setupPlugins.alerting.registerType(createEqlAlertType(ruleDataClient, this.logger));

View file

@ -11,11 +11,14 @@ import type { LicensingApiRequestHandlerContext } from '../../licensing/server';
import type { AlertingApiRequestHandlerContext } from '../../alerting/server'; import type { AlertingApiRequestHandlerContext } from '../../alerting/server';
import { AppClient } from './client'; import { AppClient } from './client';
import { RuleExecutionLogClient } from './lib/detection_engine/rule_execution_log/rule_execution_log_client';
export { AppClient }; export { AppClient };
export interface AppRequestContext { export interface AppRequestContext {
getAppClient: () => AppClient; getAppClient: () => AppClient;
getSpaceId: () => string;
getExecutionLogClient: () => RuleExecutionLogClient;
} }
export type SecuritySolutionRequestHandlerContext = RequestHandlerContext & { export type SecuritySolutionRequestHandlerContext = RequestHandlerContext & {