Implement RuleExecutionLog (#103463)
This commit is contained in:
parent
4b4525ab05
commit
fddd9d7992
|
@ -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({});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
|
@ -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';
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
23
x-pack/plugins/security_solution/common/utils/invariant.ts
Normal file
23
x-pack/plugins/security_solution/common/utils/invariant.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 => ({
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 } })
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(),
|
||||||
|
};
|
||||||
|
});
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -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>;
|
|
@ -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;
|
|
@ -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);
|
||||||
|
};
|
|
@ -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],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -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>;
|
||||||
|
}
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -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);
|
||||||
|
|
|
@ -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));
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(),
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 };
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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 {};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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 & {
|
||||||
|
|
Loading…
Reference in a new issue