Implement RuleExecutionLog (#103463)

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

View file

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

View file

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

View file

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

View file

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

View file

@ -147,6 +147,12 @@ export class RuleDataPluginService {
return;
} catch (err) {
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}`);
return;
}
@ -161,6 +167,10 @@ export class RuleDataPluginService {
new_index: newIndexName,
});
} 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') {
this.options.logger.error(`Failed to rollover index for alias ${alias}: ${e.message}`);
}

View file

@ -55,6 +55,7 @@ export const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000; // ms
export const DEFAULT_RULE_REFRESH_IDLE_VALUE = 2700000; // ms
export const DEFAULT_RULE_NOTIFICATION_QUERY_SIZE = 100;
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
// to enrich signals, and are copied to threat.indicator.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,9 +5,9 @@
* 2.0.
*/
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
? 'subdued'
: status === 'succeeded'

View file

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

View file

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

View file

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

View file

@ -14,12 +14,14 @@ import {
import { rulesClientMock } from '../../../../../../alerting/server/mocks';
import { licensingMock } from '../../../../../../licensing/server/mocks';
import { siemMock } from '../../../../mocks';
import { RuleExecutionLogClient } from '../../rule_execution_log/__mocks__/rule_execution_log_client';
const createMockClients = () => ({
rulesClient: rulesClientMock.create(),
licensing: { license: licensingMock.createLicenseMock() },
clusterClient: elasticsearchServiceMock.createScopedClusterClient(),
savedObjectsClient: savedObjectsClientMock.create(),
ruleExecutionLogClient: new RuleExecutionLogClient(),
appClient: siemMock.createClient(),
});
@ -57,7 +59,11 @@ const createRequestContextMock = (
savedObjects: { client: clients.savedObjectsClient },
},
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;
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { SavedObjectsFindResponse } from 'kibana/server';
import { SavedObjectsFindResponse, SavedObjectsFindResult } from 'kibana/server';
import { ActionResult } from '../../../../../../actions/server';
import { SignalSearchResponse } from '../../signals/types';
import {
@ -24,6 +24,7 @@ import {
RuleAlertType,
IRuleSavedAttributesSavedObjectAttributes,
HapiReadableStream,
IRuleStatusSOAttributes,
} from '../../rules/types';
import { requestMock } from './request';
import { RuleNotificationAlertType } from '../../notifications/types';
@ -37,6 +38,8 @@ import { RuleParams } from '../../schemas/rule_schemas';
import { Alert } from '../../../../../../alerting/common';
import { getQueryRuleParams } from '../../schemas/rule_schemas.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 => ({
signal_ids: ['somefakeid1', 'somefakeid2'],
@ -442,128 +445,93 @@ export const getMockPrivilegesResult = () => ({
application: {},
});
export const getFindResultStatusEmpty = (): SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> => ({
export const getEmptySavedObjectsResponse = (): SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> => ({
page: 1,
per_page: 1,
total: 0,
saved_objects: [],
});
export const getFindResultStatus = (): SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> => ({
page: 1,
per_page: 6,
total: 2,
saved_objects: [
{
type: 'my-type',
id: 'e0b86950-4e9f-11ea-bdbd-07b56aa159b3',
attributes: {
alertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bc',
statusDate: '2020-02-18T15:26:49.783Z',
status: 'succeeded',
lastFailureAt: undefined,
lastSuccessAt: '2020-02-18T15:26:49.783Z',
lastFailureMessage: undefined,
lastSuccessMessage: 'succeeded',
lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(),
gap: '500.32',
searchAfterTimeDurations: ['200.00'],
bulkCreateTimeDurations: ['800.43'],
},
score: 1,
references: [],
updated_at: '2020-02-18T15:26:51.333Z',
version: 'WzQ2LDFd',
export const getRuleExecutionStatuses = (): Array<
SavedObjectsFindResult<IRuleStatusSOAttributes>
> => [
{
type: 'my-type',
id: 'e0b86950-4e9f-11ea-bdbd-07b56aa159b3',
attributes: {
alertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bc',
statusDate: '2020-02-18T15:26:49.783Z',
status: RuleExecutionStatus.succeeded,
lastFailureAt: undefined,
lastSuccessAt: '2020-02-18T15:26:49.783Z',
lastFailureMessage: undefined,
lastSuccessMessage: 'succeeded',
lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(),
gap: '500.32',
searchAfterTimeDurations: ['200.00'],
bulkCreateTimeDurations: ['800.43'],
},
score: 1,
references: [],
updated_at: '2020-02-18T15:26:51.333Z',
version: 'WzQ2LDFd',
},
{
type: 'my-type',
id: '91246bd0-5261-11ea-9650-33b954270f67',
attributes: {
alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08',
statusDate: '2020-02-18T15:15:58.806Z',
status: RuleExecutionStatus.failed,
lastFailureAt: '2020-02-18T15:15:58.806Z',
lastSuccessAt: '2020-02-13T20:31:59.855Z',
lastFailureMessage:
'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.',
lastSuccessMessage: 'succeeded',
lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(),
gap: '500.32',
searchAfterTimeDurations: ['200.00'],
bulkCreateTimeDurations: ['800.43'],
},
score: 1,
references: [],
updated_at: '2020-02-18T15:15:58.860Z',
version: 'WzMyLDFd',
},
];
export const getFindBulkResultStatus = (): FindBulkExecutionLogResponse => ({
'04128c15-0d1b-4716-a4c5-46997ac7f3bd': [
{
type: 'my-type',
id: '91246bd0-5261-11ea-9650-33b954270f67',
attributes: {
alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08',
statusDate: '2020-02-18T15:15:58.806Z',
status: 'failed',
lastFailureAt: '2020-02-18T15:15:58.806Z',
lastSuccessAt: '2020-02-13T20:31:59.855Z',
lastFailureMessage:
'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.',
lastSuccessMessage: 'succeeded',
lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(),
gap: '500.32',
searchAfterTimeDurations: ['200.00'],
bulkCreateTimeDurations: ['800.43'],
},
score: 1,
references: [],
updated_at: '2020-02-18T15:15:58.860Z',
version: 'WzMyLDFd',
alertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
statusDate: '2020-02-18T15:26:49.783Z',
status: RuleExecutionStatus.succeeded,
lastFailureAt: undefined,
lastSuccessAt: '2020-02-18T15:26:49.783Z',
lastFailureMessage: undefined,
lastSuccessMessage: 'succeeded',
lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(),
gap: '500.32',
searchAfterTimeDurations: ['200.00'],
bulkCreateTimeDurations: ['800.43'],
},
],
});
export const getFindBulkResultStatus = (): SavedObjectsFindResponse<IRuleSavedAttributesSavedObjectAttributes> => ({
page: 1,
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',
statusDate: '2020-02-18T15:26:49.783Z',
status: 'succeeded',
lastFailureAt: undefined,
lastSuccessAt: '2020-02-18T15:26:49.783Z',
lastFailureMessage: undefined,
lastSuccessMessage: 'succeeded',
lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(),
gap: '500.32',
searchAfterTimeDurations: ['200.00'],
bulkCreateTimeDurations: ['800.43'],
},
},
},
],
},
},
},
{
key: '1ea5a820-4da1-4e82-92a1-2b43a7bece08',
most_recent_statuses: {
hits: {
hits: [
{
_source: {
'siem-detection-engine-rule-status': {
alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08',
statusDate: '2020-02-18T15:15:58.806Z',
status: 'failed',
lastFailureAt: '2020-02-18T15:15:58.806Z',
lastSuccessAt: '2020-02-13T20:31:59.855Z',
lastFailureMessage:
'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.',
lastSuccessMessage: 'succeeded',
lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(),
gap: '500.32',
searchAfterTimeDurations: ['200.00'],
bulkCreateTimeDurations: ['800.43'],
},
},
},
],
},
},
},
],
'1ea5a820-4da1-4e82-92a1-2b43a7bece08': [
{
alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08',
statusDate: '2020-02-18T15:15:58.806Z',
status: RuleExecutionStatus.failed,
lastFailureAt: '2020-02-18T15:15:58.806Z',
lastSuccessAt: '2020-02-13T20:31:59.855Z',
lastFailureMessage:
'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.',
lastSuccessMessage: 'succeeded',
lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(),
gap: '500.32',
searchAfterTimeDurations: ['200.00'],
bulkCreateTimeDurations: ['800.43'],
},
},
],
});
export const getEmptySignalsResponse = (): SignalSearchResponse => ({

View file

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

View file

@ -10,7 +10,7 @@ import {
getEmptyFindResult,
getAlertMock,
getCreateRequest,
getFindResultStatus,
getRuleExecutionStatuses,
getFindResultWithSingleHit,
createMlRuleRequest,
} from '../__mocks__/request_responses';
@ -38,7 +38,7 @@ describe('create_rules', () => {
clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); // no current rules
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(
elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } })

View file

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

View file

@ -13,8 +13,7 @@ import {
getDeleteBulkRequestById,
getDeleteAsPostBulkRequest,
getDeleteAsPostBulkRequestById,
getFindResultStatusEmpty,
getFindResultStatus,
getEmptySavedObjectsResponse,
} from '../__mocks__/request_responses';
import { requestContextMock, serverMock, requestMock } from '../__mocks__';
import { deleteRulesBulkRoute } from './delete_rules_bulk_route';
@ -29,7 +28,7 @@ describe('delete_rules', () => {
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists
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);
});
@ -41,7 +40,6 @@ describe('delete_rules', () => {
});
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);
expect(response.status).toEqual(200);
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,7 +26,7 @@ describe('find_statuses', () => {
beforeEach(async () => {
server = serverMock.create();
({ 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()));
findRulesStatusesRoute(server.router);
});
@ -45,7 +45,7 @@ describe('find_statuses', () => {
});
test('catch error when status search throws error', async () => {
clients.savedObjectsClient.find.mockImplementation(async () => {
clients.ruleExecutionLogClient.findBulk.mockImplementation(async () => {
throw new Error('Test error');
});
const response = await server.inject(ruleStatusRequest(), context);

View file

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

View file

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

View file

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

View file

@ -10,12 +10,13 @@ import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../mach
import { buildMlAuthz } from '../../../machine_learning/authz';
import {
getEmptyFindResult,
getFindResultStatus,
getRuleExecutionStatuses,
getAlertMock,
getPatchRequest,
getFindResultWithSingleHit,
nonRuleFindResult,
typicalMlRulePayload,
getEmptySavedObjectsResponse,
} from '../__mocks__/request_responses';
import { requestContextMock, serverMock, requestMock } from '../__mocks__';
import { patchRulesRoute } from './patch_rules_route';
@ -37,7 +38,9 @@ describe('patch_rules', () => {
clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); // existing rule
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // existing rule
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);
});

View file

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

View file

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

View file

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

View file

@ -12,7 +12,7 @@ import {
getReadRequest,
getFindResultWithSingleHit,
nonRuleFindResult,
getFindResultStatusEmpty,
getEmptySavedObjectsResponse,
} from '../__mocks__/request_responses';
import { requestMock, requestContextMock, serverMock } from '../__mocks__';
@ -25,7 +25,8 @@ describe('read_signals', () => {
({ clients, context } = requestContextMock.createTools());
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);
});

View file

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

View file

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

View file

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

View file

@ -12,7 +12,6 @@ import {
getAlertMock,
getUpdateRequest,
getFindResultWithSingleHit,
getFindResultStatusEmpty,
nonRuleFindResult,
typicalMlRulePayload,
} from '../__mocks__/request_responses';
@ -39,7 +38,7 @@ describe('update_rules', () => {
clients.rulesClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams())); // existing rule
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists
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);
});

View file

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

View file

@ -8,10 +8,11 @@
import { transformValidate, transformValidateBulkError } from './validate';
import { BulkError } from '../utils';
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 { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock';
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
export const ruleOutput = (): RulesSchema => ({
actions: [],
@ -107,12 +108,12 @@ describe('validate', () => {
});
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 validatedOrError = transformValidateBulkError('rule-1', ruleAlert, null, ruleStatus);
const validatedOrError = transformValidateBulkError('rule-1', ruleAlert, null, ruleStatuses);
const expected: RulesSchema = {
...ruleOutput(),
status: 'succeeded',
status: RuleExecutionStatus.succeeded,
status_date: '2020-02-18T15:26:49.783Z',
last_success_at: '2020-02-18T15:26:49.783Z',
last_success_message: 'succeeded',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Logger } from '@kbn/logging';
import { AlertInstanceContext, AlertTypeParams } from '../../../../../alerting/common';
import { AlertTypeWithExecutor, RuleDataPluginService } from '../../../../../rule_registry/server';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
import { RuleExecutionLogClient } from './rule_execution_log_client';
import { IRuleExecutionLogClient } from './types';
export interface ExecutionLogServices {
ruleExecutionLogClient: IRuleExecutionLogClient;
logger: Logger;
}
type WithRuleExecutionLog = (args: {
logger: Logger;
ruleDataService: RuleDataPluginService;
}) => <
TParams extends AlertTypeParams,
TAlertInstanceContext extends AlertInstanceContext,
TServices extends ExecutionLogServices
>(
type: AlertTypeWithExecutor<TParams, TAlertInstanceContext, TServices>
) => AlertTypeWithExecutor<TParams, TAlertInstanceContext, TServices>;
export const withRuleExecutionLogFactory: WithRuleExecutionLog = ({ logger, ruleDataService }) => (
type
) => {
return {
...type,
executor: async (options) => {
const ruleExecutionLogClient = new RuleExecutionLogClient({
ruleDataService,
savedObjectsClient: options.services.savedObjectsClient,
});
try {
await ruleExecutionLogClient.logStatusChange({
spaceId: options.spaceId,
ruleId: options.alertId,
newStatus: RuleExecutionStatus['going to run'],
});
const state = await type.executor({
...options,
services: {
...options.services,
ruleExecutionLogClient,
logger,
},
});
await ruleExecutionLogClient.logStatusChange({
spaceId: options.spaceId,
ruleId: options.alertId,
newStatus: RuleExecutionStatus.succeeded,
});
return state;
} catch (error) {
logger.error(error);
await ruleExecutionLogClient.logStatusChange({
spaceId: options.spaceId,
ruleId: options.alertId,
newStatus: RuleExecutionStatus.failed,
message: error.message,
});
}
},
};
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@ import {
SavedObjectAttributes,
SavedObjectsFindResponse,
SavedObjectsClientContract,
SavedObjectsFindResult,
} from 'kibana/server';
import type {
MachineLearningJobIdOrUndefined,
@ -86,7 +87,7 @@ import {
QueryFilterOrUndefined,
FieldsOrUndefined,
SortOrderOrUndefined,
JobStatus,
RuleExecutionStatus,
LastSuccessAt,
StatusDate,
LastSuccessMessage,
@ -106,7 +107,7 @@ import { Alert, SanitizedAlert } from '../../../../../alerting/common';
import { SIGNALS_ID } from '../../../../common/constants';
import { PartialFilter } from '../types';
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>;
@ -118,7 +119,7 @@ export interface IRuleStatusSOAttributes extends Record<string, any> {
lastFailureMessage: LastFailureMessage | null | undefined;
lastSuccessAt: LastSuccessAt | null | undefined;
lastSuccessMessage: LastSuccessMessage | null | undefined;
status: JobStatus | null | undefined;
status: RuleExecutionStatus | null | undefined;
lastLookBackDate: string | null | undefined;
gap: string | null | undefined;
bulkCreateTimeDurations: string[] | null | undefined;
@ -132,7 +133,7 @@ export interface IRuleStatusResponseAttributes {
last_failure_message: LastFailureMessage | null | undefined;
last_success_at: LastSuccessAt | 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
gap: string | null | undefined;
bulk_create_time_durations: string[] | null | undefined;
@ -266,14 +267,16 @@ export interface CreateRulesOptions {
}
export interface UpdateRulesOptions {
savedObjectsClient: SavedObjectsClientContract;
spaceId: string;
ruleStatusClient: IRuleExecutionLogClient;
rulesClient: RulesClient;
defaultOutputIndex: string;
ruleUpdate: UpdateRulesSchema;
}
export interface PatchRulesOptions {
savedObjectsClient: SavedObjectsClientContract;
spaceId: string;
ruleStatusClient: IRuleExecutionLogClient;
rulesClient: RulesClient;
anomalyThreshold: AnomalyThresholdOrUndefined;
author: AuthorOrUndefined;
@ -332,8 +335,8 @@ export interface ReadRuleOptions {
export interface DeleteRuleOptions {
rulesClient: RulesClient;
savedObjectsClient: SavedObjectsClientContract;
ruleStatusClient: RuleStatusSavedObjectsClient;
ruleStatuses: SavedObjectsFindResponse<IRuleStatusSOAttributes, unknown>;
ruleStatusClient: IRuleExecutionLogClient;
ruleStatuses: Array<SavedObjectsFindResult<IRuleStatusSOAttributes>>;
id: Id;
}

View file

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

View file

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

View file

@ -5,24 +5,26 @@
* 2.0.
*/
import { UpdateRulesOptions } from './types';
import { rulesClientMock } from '../../../../../alerting/server/mocks';
import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks';
import {
getUpdateRulesSchemaMock,
getUpdateMachineLearningSchemaMock,
getUpdateRulesSchemaMock,
} 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 => ({
spaceId: 'default',
rulesClient: rulesClientMock.create(),
savedObjectsClient: savedObjectsClientMock.create(),
ruleStatusClient: new RuleExecutionLogClient(),
defaultOutputIndex: '.siem-signals-default',
ruleUpdate: getUpdateRulesSchemaMock(),
});
export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({
spaceId: 'default',
rulesClient: rulesClientMock.create(),
savedObjectsClient: savedObjectsClientMock.create(),
ruleStatusClient: new RuleExecutionLogClient(),
defaultOutputIndex: '.siem-signals-default',
ruleUpdate: getUpdateMachineLearningSchemaMock(),
});

View file

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

View file

@ -31,6 +31,7 @@ import { transformRuleToAlertAction } from '../../../../common/detection_engine/
import { SanitizedAlert } from '../../../../../alerting/common';
import { IRuleStatusSOAttributes } from '../rules/types';
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
// 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}`,
lastFailureAt: alert.executionStatus.lastExecutionDate.toISOString(),
statusDate: alert.executionStatus.lastExecutionDate.toISOString(),
status: 'failed',
status: RuleExecutionStatus.failed,
};
}
return status;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@ import {
MAX_RULE_STATUSES,
} from './rule_status_service';
import { exampleRuleStatus, exampleFindRuleStatusResponse } from './__mocks__/es_results';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
const expectIsoDateString = expect.stringMatching(/2.*Z$/);
const buildStatuses = (n: number) =>
@ -25,9 +26,11 @@ const buildStatuses = (n: number) =>
describe('buildRuleStatusAttributes', () => {
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
const { statusDate: statusDate2 } = buildRuleStatusAttributes('going to run');
const { statusDate: statusDate2 } = buildRuleStatusAttributes(
RuleExecutionStatus['going to run']
);
expect(statusDate).toEqual(expectIsoDateString);
expect(statusDate2).toEqual(expectIsoDateString);
@ -35,7 +38,7 @@ describe('buildRuleStatusAttributes', () => {
});
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({
status: 'going to run',
statusDate: expectIsoDateString,
@ -43,7 +46,7 @@ describe('buildRuleStatusAttributes', () => {
});
it('returns success fields if "success"', () => {
const result = buildRuleStatusAttributes('succeeded', 'success message');
const result = buildRuleStatusAttributes(RuleExecutionStatus.succeeded, 'success message');
expect(result).toEqual({
status: 'succeeded',
statusDate: expectIsoDateString,
@ -56,7 +59,7 @@ describe('buildRuleStatusAttributes', () => {
it('returns warning fields if "warning"', () => {
const result = buildRuleStatusAttributes(
'warning',
RuleExecutionStatus.warning,
'some indices missing timestamp override field'
);
expect(result).toEqual({
@ -70,7 +73,7 @@ describe('buildRuleStatusAttributes', () => {
});
it('returns failure fields if "failed"', () => {
const result = buildRuleStatusAttributes('failed', 'failure message');
const result = buildRuleStatusAttributes(RuleExecutionStatus.failed, 'failure message');
expect(result).toEqual({
status: 'failed',
statusDate: expectIsoDateString,

View file

@ -6,7 +6,7 @@
*/
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 { getOrCreateRuleStatuses } from './get_or_create_rule_statuses';
import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client';
@ -29,7 +29,7 @@ export interface RuleStatusService {
}
export const buildRuleStatusAttributes: (
status: JobStatus,
status: RuleExecutionStatus,
message?: string,
attributes?: Attributes
) => Partial<IRuleStatusSOAttributes> = (status, message, attributes = {}) => {
@ -41,35 +41,35 @@ export const buildRuleStatusAttributes: (
};
switch (status) {
case 'succeeded': {
case RuleExecutionStatus.succeeded: {
return {
...baseAttributes,
lastSuccessAt: now,
lastSuccessMessage: message,
};
}
case 'warning': {
case RuleExecutionStatus.warning: {
return {
...baseAttributes,
lastSuccessAt: now,
lastSuccessMessage: message,
};
}
case 'partial failure': {
case RuleExecutionStatus['partial failure']: {
return {
...baseAttributes,
lastSuccessAt: now,
lastSuccessMessage: message,
};
}
case 'failed': {
case RuleExecutionStatus.failed: {
return {
...baseAttributes,
lastFailureAt: now,
lastFailureMessage: message,
};
}
case 'going to run': {
case RuleExecutionStatus['going to run']: {
return baseAttributes;
}
}
@ -93,7 +93,7 @@ export const ruleStatusServiceFactory = async ({
await ruleStatusClient.update(currentStatus.id, {
...currentStatus.attributes,
...buildRuleStatusAttributes('going to run'),
...buildRuleStatusAttributes(RuleExecutionStatus['going to run']),
});
},
@ -105,7 +105,7 @@ export const ruleStatusServiceFactory = async ({
await ruleStatusClient.update(currentStatus.id, {
...currentStatus.attributes,
...buildRuleStatusAttributes('succeeded', message, attributes),
...buildRuleStatusAttributes(RuleExecutionStatus.succeeded, message, attributes),
});
},
@ -117,7 +117,7 @@ export const ruleStatusServiceFactory = async ({
await ruleStatusClient.update(currentStatus.id, {
...currentStatus.attributes,
...buildRuleStatusAttributes('partial failure', message, attributes),
...buildRuleStatusAttributes(RuleExecutionStatus['partial failure'], message, attributes),
});
},
@ -130,7 +130,7 @@ export const ruleStatusServiceFactory = async ({
const failureAttributes = {
...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

View file

@ -69,6 +69,7 @@ import {
REFERENCE_RULE_ALERT_TYPE_ID,
REFERENCE_RULE_PERSISTENCE_ALERT_TYPE_ID,
CUSTOM_ALERT_TYPE_ID,
DEFAULT_SPACE_ID,
} from '../common/constants';
import { registerEndpointRoutes } from './endpoint/routes/metadata';
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 { parseExperimentalConfigValue } from '../common/experimental_features';
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 { EndpointMetadataService } from './endpoint/services/metadata';
@ -184,6 +186,13 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
APP_ID,
(context, request, response) => ({
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 initializeRuleDataTemplates = once(async () => {
const componentTemplateName = ruleDataService.getFullAssetName('security.alerts-mappings');
if (!ruleDataService.isWriteEnabled()) {
return;
}
const componentTemplateName = ruleDataService.getFullAssetName('security.alerts-mappings');
await ruleDataService.createOrUpdateComponentTemplate({
name: componentTemplateName,
@ -245,8 +253,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
() => initializeRuleDataTemplatesPromise
);
// sec
// Register reference rule types via rule-registry
this.setupPlugins.alerting.registerType(createQueryAlertType(ruleDataClient, this.logger));
this.setupPlugins.alerting.registerType(createEqlAlertType(ruleDataClient, this.logger));

View file

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