mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Implement RuleExecutionLog (#103463)
This commit is contained in:
parent
4b4525ab05
commit
fddd9d7992
76 changed files with 1384 additions and 396 deletions
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { enumeration } from '.';
|
||||
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
|
||||
|
||||
describe('enumeration', () => {
|
||||
enum TestEnum {
|
||||
'test' = 'test',
|
||||
}
|
||||
|
||||
it('should validate a string from the enum', () => {
|
||||
const input = TestEnum.test;
|
||||
const codec = enumeration('TestEnum', TestEnum);
|
||||
const decoded = codec.decode(input);
|
||||
const message = foldLeftRight(decoded);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(input);
|
||||
});
|
||||
|
||||
it('should NOT validate a random string', () => {
|
||||
const input = 'some string';
|
||||
const codec = enumeration('TestEnum', TestEnum);
|
||||
const decoded = codec.decode(input);
|
||||
const message = foldLeftRight(decoded);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "some string" supplied to "TestEnum"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
export function enumeration<EnumType extends string>(
|
||||
name: string,
|
||||
originalEnum: Record<string, EnumType>
|
||||
): t.Type<EnumType, EnumType, unknown> {
|
||||
const isEnumValue = (input: unknown): input is EnumType =>
|
||||
Object.values<unknown>(originalEnum).includes(input);
|
||||
|
||||
return new t.Type<EnumType>(
|
||||
name,
|
||||
isEnumValue,
|
||||
(input, context) => (isEnumValue(input) ? t.success(input) : t.failure(input, context)),
|
||||
t.identity
|
||||
);
|
||||
}
|
|
@ -15,15 +15,16 @@ export * from './default_string_boolean_false';
|
|||
export * from './default_uuid';
|
||||
export * from './default_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';
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
23
x-pack/plugins/security_solution/common/utils/invariant.ts
Normal file
23
x-pack/plugins/security_solution/common/utils/invariant.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export class InvariantError extends Error {
|
||||
name = 'Invariant violation';
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the provided condition is always true
|
||||
* and throws an invariant violation error otherwise
|
||||
*
|
||||
* @param condition Condition to assert
|
||||
* @param message Error message to throw if the condition is falsy
|
||||
*/
|
||||
export function invariant(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new InvariantError(message);
|
||||
}
|
||||
}
|
|
@ -5,9 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
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'
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 } })
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
]);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IRuleExecutionLogClient } from '../types';
|
||||
|
||||
export const RuleExecutionLogClient = jest
|
||||
.fn<jest.Mocked<IRuleExecutionLogClient>, []>()
|
||||
.mockImplementation(() => {
|
||||
return {
|
||||
find: jest.fn(),
|
||||
findBulk: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
logStatusChange: jest.fn(),
|
||||
logExecutionMetric: jest.fn(),
|
||||
};
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { merge } from 'lodash';
|
||||
import { RuleDataPluginService } from '../../../../../../rule_registry/server';
|
||||
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { IRuleStatusSOAttributes } from '../../rules/types';
|
||||
import { RuleRegistryLogClient } from '../rule_registry_log_client/rule_registry_log_client';
|
||||
import {
|
||||
ExecutionMetric,
|
||||
ExecutionMetricArgs,
|
||||
FindBulkExecutionLogArgs,
|
||||
FindExecutionLogArgs,
|
||||
IRuleExecutionLogClient,
|
||||
LogStatusChangeArgs,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* @deprecated RuleRegistryAdapter is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*/
|
||||
export class RuleRegistryAdapter implements IRuleExecutionLogClient {
|
||||
private ruleRegistryClient: RuleRegistryLogClient;
|
||||
|
||||
constructor(ruleDataService: RuleDataPluginService) {
|
||||
this.ruleRegistryClient = new RuleRegistryLogClient(ruleDataService);
|
||||
}
|
||||
|
||||
public async find({ ruleId, logsCount = 1, spaceId }: FindExecutionLogArgs) {
|
||||
const logs = await this.ruleRegistryClient.find({
|
||||
ruleIds: [ruleId],
|
||||
logsCount,
|
||||
spaceId,
|
||||
});
|
||||
|
||||
return logs[ruleId].map((log) => ({
|
||||
id: '',
|
||||
type: '',
|
||||
score: 0,
|
||||
attributes: log,
|
||||
references: [],
|
||||
}));
|
||||
}
|
||||
|
||||
public async findBulk({ ruleIds, logsCount = 1, spaceId }: FindBulkExecutionLogArgs) {
|
||||
const [statusesById, lastErrorsById] = await Promise.all([
|
||||
this.ruleRegistryClient.find({ ruleIds, spaceId }),
|
||||
this.ruleRegistryClient.find({
|
||||
ruleIds,
|
||||
statuses: [RuleExecutionStatus.failed],
|
||||
logsCount,
|
||||
spaceId,
|
||||
}),
|
||||
]);
|
||||
return merge(statusesById, lastErrorsById);
|
||||
}
|
||||
|
||||
public async create(event: IRuleStatusSOAttributes, spaceId: string) {
|
||||
if (event.status) {
|
||||
await this.ruleRegistryClient.logStatusChange({
|
||||
ruleId: event.alertId,
|
||||
newStatus: event.status,
|
||||
spaceId,
|
||||
});
|
||||
}
|
||||
|
||||
if (event.bulkCreateTimeDurations) {
|
||||
await this.ruleRegistryClient.logExecutionMetric({
|
||||
ruleId: event.alertId,
|
||||
metric: ExecutionMetric.indexingDurationMax,
|
||||
value: Math.max(...event.bulkCreateTimeDurations.map(Number)),
|
||||
spaceId,
|
||||
});
|
||||
}
|
||||
|
||||
if (event.gap) {
|
||||
await this.ruleRegistryClient.logExecutionMetric({
|
||||
ruleId: event.alertId,
|
||||
metric: ExecutionMetric.executionGap,
|
||||
value: Number(event.gap),
|
||||
spaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async update(id: string, event: IRuleStatusSOAttributes, spaceId: string) {
|
||||
// execution events are immutable, so we just use 'create' here instead of 'update'
|
||||
await this.create(event, spaceId);
|
||||
}
|
||||
|
||||
public async delete(id: string) {
|
||||
// execution events are immutable, nothing to do here
|
||||
}
|
||||
|
||||
public async logExecutionMetric<T extends ExecutionMetric>(args: ExecutionMetricArgs<T>) {
|
||||
return this.ruleRegistryClient.logExecutionMetric(args);
|
||||
}
|
||||
|
||||
public async logStatusChange(args: LogStatusChangeArgs) {
|
||||
return this.ruleRegistryClient.logStatusChange(args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObjectsClientContract } from '../../../../../../../../src/core/server';
|
||||
import { IRuleStatusSOAttributes } from '../../rules/types';
|
||||
import {
|
||||
RuleStatusSavedObjectsClient,
|
||||
ruleStatusSavedObjectsClientFactory,
|
||||
} from '../../signals/rule_status_saved_objects_client';
|
||||
import {
|
||||
ExecutionMetric,
|
||||
ExecutionMetricArgs,
|
||||
FindBulkExecutionLogArgs,
|
||||
FindExecutionLogArgs,
|
||||
IRuleExecutionLogClient,
|
||||
LogStatusChangeArgs,
|
||||
} from '../types';
|
||||
|
||||
export class SavedObjectsAdapter implements IRuleExecutionLogClient {
|
||||
private ruleStatusClient: RuleStatusSavedObjectsClient;
|
||||
|
||||
constructor(savedObjectsClient: SavedObjectsClientContract) {
|
||||
this.ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
|
||||
}
|
||||
|
||||
public find({ ruleId, logsCount = 1 }: FindExecutionLogArgs) {
|
||||
return this.ruleStatusClient.find({
|
||||
perPage: logsCount,
|
||||
sortField: 'statusDate',
|
||||
sortOrder: 'desc',
|
||||
search: ruleId,
|
||||
searchFields: ['alertId'],
|
||||
});
|
||||
}
|
||||
|
||||
public findBulk({ ruleIds, logsCount = 1 }: FindBulkExecutionLogArgs) {
|
||||
return this.ruleStatusClient.findBulk(ruleIds, logsCount);
|
||||
}
|
||||
|
||||
public async create(event: IRuleStatusSOAttributes) {
|
||||
await this.ruleStatusClient.create(event);
|
||||
}
|
||||
|
||||
public async update(id: string, event: IRuleStatusSOAttributes) {
|
||||
await this.ruleStatusClient.update(id, event);
|
||||
}
|
||||
|
||||
public async delete(id: string) {
|
||||
await this.ruleStatusClient.delete(id);
|
||||
}
|
||||
|
||||
public async logExecutionMetric<T extends ExecutionMetric>(args: ExecutionMetricArgs<T>) {
|
||||
// TODO These methods are intended to supersede ones provided by RuleStatusService
|
||||
}
|
||||
|
||||
public async logStatusChange(args: LogStatusChangeArgs) {
|
||||
// TODO These methods are intended to supersede ones provided by RuleStatusService
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObjectsClientContract } from '../../../../../../../src/core/server';
|
||||
import { RuleDataPluginService } from '../../../../../rule_registry/server';
|
||||
import { IRuleStatusSOAttributes } from '../rules/types';
|
||||
import { RuleRegistryAdapter } from './adapters/rule_registry_dapter';
|
||||
import { SavedObjectsAdapter } from './adapters/saved_objects_adapter';
|
||||
import {
|
||||
ExecutionMetric,
|
||||
ExecutionMetricArgs,
|
||||
FindBulkExecutionLogArgs,
|
||||
FindExecutionLogArgs,
|
||||
IRuleExecutionLogClient,
|
||||
LogStatusChangeArgs,
|
||||
} from './types';
|
||||
|
||||
export interface RuleExecutionLogClientArgs {
|
||||
ruleDataService: RuleDataPluginService;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
}
|
||||
|
||||
const RULE_REGISTRY_LOG_ENABLED = false;
|
||||
|
||||
export class RuleExecutionLogClient implements IRuleExecutionLogClient {
|
||||
private client: IRuleExecutionLogClient;
|
||||
|
||||
constructor({ ruleDataService, savedObjectsClient }: RuleExecutionLogClientArgs) {
|
||||
if (RULE_REGISTRY_LOG_ENABLED) {
|
||||
this.client = new RuleRegistryAdapter(ruleDataService);
|
||||
} else {
|
||||
this.client = new SavedObjectsAdapter(savedObjectsClient);
|
||||
}
|
||||
}
|
||||
|
||||
public find(args: FindExecutionLogArgs) {
|
||||
return this.client.find(args);
|
||||
}
|
||||
|
||||
public findBulk(args: FindBulkExecutionLogArgs) {
|
||||
return this.client.findBulk(args);
|
||||
}
|
||||
|
||||
// TODO args as an object
|
||||
public async create(event: IRuleStatusSOAttributes, spaceId: string) {
|
||||
return this.client.create(event, spaceId);
|
||||
}
|
||||
|
||||
// TODO args as an object
|
||||
public async update(id: string, event: IRuleStatusSOAttributes, spaceId: string) {
|
||||
return this.client.update(id, event, spaceId);
|
||||
}
|
||||
|
||||
public async delete(id: string) {
|
||||
return this.client.delete(id);
|
||||
}
|
||||
|
||||
public async logExecutionMetric<T extends ExecutionMetric>(args: ExecutionMetricArgs<T>) {
|
||||
return this.client.logExecutionMetric(args);
|
||||
}
|
||||
|
||||
public async logStatusChange(args: LogStatusChangeArgs) {
|
||||
return this.client.logStatusChange(args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated EVENTS_INDEX_PREFIX is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*/
|
||||
export const EVENTS_INDEX_PREFIX = '.kibana_alerts-security.events';
|
||||
|
||||
/**
|
||||
* @deprecated MESSAGE is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*/
|
||||
export const MESSAGE = 'message' as const;
|
||||
|
||||
/**
|
||||
* @deprecated EVENT_SEQUENCE is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*/
|
||||
export const EVENT_SEQUENCE = 'event.sequence' as const;
|
||||
|
||||
/**
|
||||
* @deprecated EVENT_DURATION is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*/
|
||||
export const EVENT_DURATION = 'event.duration' as const;
|
||||
|
||||
/**
|
||||
* @deprecated EVENT_END is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*/
|
||||
export const EVENT_END = 'event.end' as const;
|
||||
|
||||
/**
|
||||
* @deprecated RULE_STATUS is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*/
|
||||
export const RULE_STATUS = 'kibana.rac.detection_engine.rule_status' as const;
|
||||
|
||||
/**
|
||||
* @deprecated RULE_STATUS_SEVERITY is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*/
|
||||
export const RULE_STATUS_SEVERITY = 'kibana.rac.detection_engine.rule_status_severity' as const;
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isLeft } from 'fp-ts/lib/Either';
|
||||
import { PathReporter } from 'io-ts/lib/PathReporter';
|
||||
import { technicalRuleFieldMap } from '../../../../../../rule_registry/common/assets/field_maps/technical_rule_field_map';
|
||||
import {
|
||||
mergeFieldMaps,
|
||||
runtimeTypeFromFieldMap,
|
||||
} from '../../../../../../rule_registry/common/field_map';
|
||||
import { ruleExecutionFieldMap } from './rule_execution_field_map';
|
||||
|
||||
const ruleExecutionLogRuntimeType = runtimeTypeFromFieldMap(
|
||||
mergeFieldMaps(technicalRuleFieldMap, ruleExecutionFieldMap)
|
||||
);
|
||||
|
||||
/**
|
||||
* @deprecated parseRuleExecutionLog is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*/
|
||||
export const parseRuleExecutionLog = (input: unknown) => {
|
||||
const validate = ruleExecutionLogRuntimeType.decode(input);
|
||||
|
||||
if (isLeft(validate)) {
|
||||
throw new Error(PathReporter.report(validate).join('\n'));
|
||||
}
|
||||
|
||||
return ruleExecutionLogRuntimeType.encode(validate.right);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated RuleExecutionEvent is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*/
|
||||
export type RuleExecutionEvent = ReturnType<typeof parseRuleExecutionLog>;
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EVENT_DURATION,
|
||||
EVENT_END,
|
||||
EVENT_SEQUENCE,
|
||||
MESSAGE,
|
||||
RULE_STATUS,
|
||||
RULE_STATUS_SEVERITY,
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* @deprecated ruleExecutionFieldMap is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*/
|
||||
export const ruleExecutionFieldMap = {
|
||||
[MESSAGE]: { type: 'keyword' },
|
||||
[EVENT_SEQUENCE]: { type: 'long' },
|
||||
[EVENT_END]: { type: 'date' },
|
||||
[EVENT_DURATION]: { type: 'long' },
|
||||
[RULE_STATUS]: { type: 'keyword' },
|
||||
[RULE_STATUS_SEVERITY]: { type: 'integer' },
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* @deprecated RuleExecutionFieldMap is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*/
|
||||
export type RuleExecutionFieldMap = typeof ruleExecutionFieldMap;
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../../../../../rule_registry/common/assets';
|
||||
import { mappingFromFieldMap } from '../../../../../../rule_registry/common/mapping_from_field_map';
|
||||
import { RuleDataPluginService } from '../../../../../../rule_registry/server';
|
||||
import { ruleExecutionFieldMap } from './rule_execution_field_map';
|
||||
|
||||
/**
|
||||
* @deprecated bootstrapRuleExecutionLog is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*/
|
||||
export const bootstrapRuleExecutionLog = async (
|
||||
ruleDataService: RuleDataPluginService,
|
||||
indexAlias: string
|
||||
) => {
|
||||
const indexPattern = `${indexAlias}*`;
|
||||
const componentTemplateName = `${indexAlias}-mappings`;
|
||||
const indexTemplateName = `${indexAlias}-template`;
|
||||
|
||||
await ruleDataService.createOrUpdateComponentTemplate({
|
||||
name: componentTemplateName,
|
||||
body: {
|
||||
template: {
|
||||
settings: {
|
||||
number_of_shards: 1,
|
||||
},
|
||||
mappings: mappingFromFieldMap(ruleExecutionFieldMap),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await ruleDataService.createOrUpdateIndexTemplate({
|
||||
name: indexTemplateName,
|
||||
body: {
|
||||
index_patterns: [indexPattern],
|
||||
composed_of: [
|
||||
ruleDataService.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME),
|
||||
componentTemplateName,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await ruleDataService.updateIndexMappingsMatchingPattern(indexPattern);
|
||||
};
|
|
@ -0,0 +1,252 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { EVENT_ACTION, EVENT_KIND, RULE_ID, SPACE_IDS, TIMESTAMP } from '@kbn/rule-data-utils';
|
||||
import { once } from 'lodash/fp';
|
||||
import moment from 'moment';
|
||||
import { RuleDataClient, RuleDataPluginService } from '../../../../../../rule_registry/server';
|
||||
import { SERVER_APP_ID } from '../../../../../common/constants';
|
||||
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { invariant } from '../../../../../common/utils/invariant';
|
||||
import { IRuleStatusSOAttributes } from '../../rules/types';
|
||||
import { makeFloatString } from '../../signals/utils';
|
||||
import { ExecutionMetric, ExecutionMetricArgs, LogStatusChangeArgs } from '../types';
|
||||
import {
|
||||
EVENTS_INDEX_PREFIX,
|
||||
MESSAGE,
|
||||
EVENT_SEQUENCE,
|
||||
RULE_STATUS,
|
||||
RULE_STATUS_SEVERITY,
|
||||
} from './constants';
|
||||
import { parseRuleExecutionLog, RuleExecutionEvent } from './parse_rule_execution_log';
|
||||
import { bootstrapRuleExecutionLog } from './rule_execution_log_bootstrapper';
|
||||
import {
|
||||
getLastEntryAggregation,
|
||||
getMetricAggregation,
|
||||
getMetricField,
|
||||
sortByTimeDesc,
|
||||
} from './utils';
|
||||
|
||||
const statusSeverityDict: Record<RuleExecutionStatus, number> = {
|
||||
[RuleExecutionStatus.succeeded]: 0,
|
||||
[RuleExecutionStatus['going to run']]: 10,
|
||||
[RuleExecutionStatus.warning]: 20,
|
||||
[RuleExecutionStatus['partial failure']]: 20,
|
||||
[RuleExecutionStatus.failed]: 30,
|
||||
};
|
||||
|
||||
interface FindExecutionLogArgs {
|
||||
ruleIds: string[];
|
||||
spaceId: string;
|
||||
logsCount?: number;
|
||||
statuses?: RuleExecutionStatus[];
|
||||
}
|
||||
|
||||
interface IRuleRegistryLogClient {
|
||||
find: (
|
||||
args: FindExecutionLogArgs
|
||||
) => Promise<{
|
||||
[ruleId: string]: IRuleStatusSOAttributes[] | undefined;
|
||||
}>;
|
||||
create: (event: RuleExecutionEvent) => Promise<void>;
|
||||
logStatusChange: (args: LogStatusChangeArgs) => Promise<void>;
|
||||
logExecutionMetric: <T extends ExecutionMetric>(args: ExecutionMetricArgs<T>) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated RuleRegistryLogClient is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*/
|
||||
export class RuleRegistryLogClient implements IRuleRegistryLogClient {
|
||||
private sequence = 0;
|
||||
private ruleDataClient: RuleDataClient;
|
||||
|
||||
constructor(ruleDataService: RuleDataPluginService) {
|
||||
this.ruleDataClient = ruleDataService.getRuleDataClient(
|
||||
SERVER_APP_ID,
|
||||
EVENTS_INDEX_PREFIX,
|
||||
() => this.initialize(ruleDataService, EVENTS_INDEX_PREFIX)
|
||||
);
|
||||
}
|
||||
|
||||
private initialize = once(async (ruleDataService: RuleDataPluginService, indexAlias: string) => {
|
||||
await bootstrapRuleExecutionLog(ruleDataService, indexAlias);
|
||||
});
|
||||
|
||||
public async find({ ruleIds, spaceId, statuses, logsCount = 1 }: FindExecutionLogArgs) {
|
||||
if (ruleIds.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const filter: estypes.QueryDslQueryContainer[] = [
|
||||
{ terms: { [RULE_ID]: ruleIds } },
|
||||
{ terms: { [SPACE_IDS]: [spaceId] } },
|
||||
];
|
||||
|
||||
if (statuses) {
|
||||
filter.push({ terms: { [RULE_STATUS]: statuses } });
|
||||
}
|
||||
|
||||
const result = await this.ruleDataClient.getReader().search({
|
||||
size: 0,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
rules: {
|
||||
terms: {
|
||||
field: RULE_ID,
|
||||
size: ruleIds.length,
|
||||
},
|
||||
aggs: {
|
||||
most_recent_logs: {
|
||||
top_hits: {
|
||||
sort: sortByTimeDesc,
|
||||
size: logsCount,
|
||||
},
|
||||
},
|
||||
last_failure: getLastEntryAggregation(RuleExecutionStatus.failed),
|
||||
last_success: getLastEntryAggregation(RuleExecutionStatus.succeeded),
|
||||
execution_gap: getMetricAggregation(ExecutionMetric.executionGap),
|
||||
search_duration_max: getMetricAggregation(ExecutionMetric.searchDurationMax),
|
||||
indexing_duration_max: getMetricAggregation(ExecutionMetric.indexingDurationMax),
|
||||
indexing_lookback: getMetricAggregation(ExecutionMetric.indexingLookback),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (result.hits.total.value === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
invariant(result.aggregations, 'Search response should contain aggregations');
|
||||
|
||||
return Object.fromEntries(
|
||||
result.aggregations.rules.buckets.map((bucket) => [
|
||||
bucket.key,
|
||||
bucket.most_recent_logs.hits.hits.map<IRuleStatusSOAttributes>((event) => {
|
||||
const logEntry = parseRuleExecutionLog(event._source);
|
||||
invariant(logEntry['rule.id'], 'Malformed execution log entry: rule.id field not found');
|
||||
|
||||
const lastFailure = bucket.last_failure.event.hits.hits[0]
|
||||
? parseRuleExecutionLog(bucket.last_failure.event.hits.hits[0]._source)
|
||||
: undefined;
|
||||
|
||||
const lastSuccess = bucket.last_success.event.hits.hits[0]
|
||||
? parseRuleExecutionLog(bucket.last_success.event.hits.hits[0]._source)
|
||||
: undefined;
|
||||
|
||||
const lookBack = bucket.indexing_lookback.event.hits.hits[0]
|
||||
? parseRuleExecutionLog(bucket.indexing_lookback.event.hits.hits[0]._source)
|
||||
: undefined;
|
||||
|
||||
const executionGap = bucket.execution_gap.event.hits.hits[0]
|
||||
? parseRuleExecutionLog(bucket.execution_gap.event.hits.hits[0]._source)[
|
||||
getMetricField(ExecutionMetric.executionGap)
|
||||
]
|
||||
: undefined;
|
||||
|
||||
const searchDuration = bucket.search_duration_max.event.hits.hits[0]
|
||||
? parseRuleExecutionLog(bucket.search_duration_max.event.hits.hits[0]._source)[
|
||||
getMetricField(ExecutionMetric.searchDurationMax)
|
||||
]
|
||||
: undefined;
|
||||
|
||||
const indexingDuration = bucket.indexing_duration_max.event.hits.hits[0]
|
||||
? parseRuleExecutionLog(bucket.indexing_duration_max.event.hits.hits[0]._source)[
|
||||
getMetricField(ExecutionMetric.indexingDurationMax)
|
||||
]
|
||||
: undefined;
|
||||
|
||||
const alertId = logEntry['rule.id'];
|
||||
const statusDate = logEntry[TIMESTAMP];
|
||||
const lastFailureAt = lastFailure?.[TIMESTAMP];
|
||||
const lastFailureMessage = lastFailure?.[MESSAGE];
|
||||
const lastSuccessAt = lastSuccess?.[TIMESTAMP];
|
||||
const lastSuccessMessage = lastSuccess?.[MESSAGE];
|
||||
const status = (logEntry[RULE_STATUS] as RuleExecutionStatus) || null;
|
||||
const lastLookBackDate = lookBack?.[getMetricField(ExecutionMetric.indexingLookback)];
|
||||
const gap = executionGap ? moment.duration(executionGap).humanize() : null;
|
||||
const bulkCreateTimeDurations = indexingDuration
|
||||
? [makeFloatString(indexingDuration)]
|
||||
: null;
|
||||
const searchAfterTimeDurations = searchDuration
|
||||
? [makeFloatString(searchDuration)]
|
||||
: null;
|
||||
|
||||
return {
|
||||
alertId,
|
||||
statusDate,
|
||||
lastFailureAt,
|
||||
lastFailureMessage,
|
||||
lastSuccessAt,
|
||||
lastSuccessMessage,
|
||||
status,
|
||||
lastLookBackDate,
|
||||
gap,
|
||||
bulkCreateTimeDurations,
|
||||
searchAfterTimeDurations,
|
||||
};
|
||||
}),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
public async logExecutionMetric<T extends ExecutionMetric>({
|
||||
ruleId,
|
||||
namespace,
|
||||
metric,
|
||||
value,
|
||||
spaceId,
|
||||
}: ExecutionMetricArgs<T>) {
|
||||
await this.create(
|
||||
{
|
||||
[SPACE_IDS]: [spaceId],
|
||||
[EVENT_ACTION]: metric,
|
||||
[EVENT_KIND]: 'metric',
|
||||
[getMetricField(metric)]: value,
|
||||
[RULE_ID]: ruleId,
|
||||
[TIMESTAMP]: new Date().toISOString(),
|
||||
},
|
||||
namespace
|
||||
);
|
||||
}
|
||||
|
||||
public async logStatusChange({
|
||||
ruleId,
|
||||
newStatus,
|
||||
namespace,
|
||||
message,
|
||||
spaceId,
|
||||
}: LogStatusChangeArgs) {
|
||||
await this.create(
|
||||
{
|
||||
[SPACE_IDS]: [spaceId],
|
||||
[EVENT_ACTION]: 'status-change',
|
||||
[EVENT_KIND]: 'event',
|
||||
[EVENT_SEQUENCE]: this.sequence++,
|
||||
[MESSAGE]: message,
|
||||
[RULE_ID]: ruleId,
|
||||
[RULE_STATUS_SEVERITY]: statusSeverityDict[newStatus],
|
||||
[RULE_STATUS]: newStatus,
|
||||
[TIMESTAMP]: new Date().toISOString(),
|
||||
},
|
||||
namespace
|
||||
);
|
||||
}
|
||||
|
||||
public async create(event: RuleExecutionEvent, namespace?: string) {
|
||||
await this.ruleDataClient.getWriter({ namespace }).bulk({
|
||||
body: [{ index: {} }, event],
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SearchSort } from '@elastic/elasticsearch/api/types';
|
||||
import { EVENT_ACTION, TIMESTAMP } from '@kbn/rule-data-utils';
|
||||
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { ExecutionMetric } from '../types';
|
||||
import { RULE_STATUS, EVENT_SEQUENCE, EVENT_DURATION, EVENT_END } from './constants';
|
||||
|
||||
const METRIC_FIELDS = {
|
||||
[ExecutionMetric.executionGap]: EVENT_DURATION,
|
||||
[ExecutionMetric.searchDurationMax]: EVENT_DURATION,
|
||||
[ExecutionMetric.indexingDurationMax]: EVENT_DURATION,
|
||||
[ExecutionMetric.indexingLookback]: EVENT_END,
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns ECS field in which metric value is stored
|
||||
* @deprecated getMetricField is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*
|
||||
* @param metric - execution metric
|
||||
* @returns ECS field
|
||||
*/
|
||||
export const getMetricField = <T extends ExecutionMetric>(metric: T) => METRIC_FIELDS[metric];
|
||||
|
||||
/**
|
||||
* @deprecated sortByTimeDesc is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*/
|
||||
export const sortByTimeDesc: SearchSort = [{ [TIMESTAMP]: 'desc' }, { [EVENT_SEQUENCE]: 'desc' }];
|
||||
|
||||
/**
|
||||
* Builds aggregation to retrieve the most recent metric value
|
||||
* @deprecated getMetricAggregation is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*
|
||||
* @param metric - execution metric
|
||||
* @returns aggregation
|
||||
*/
|
||||
export const getMetricAggregation = (metric: ExecutionMetric) => ({
|
||||
filter: {
|
||||
term: { [EVENT_ACTION]: metric },
|
||||
},
|
||||
aggs: {
|
||||
event: {
|
||||
top_hits: {
|
||||
size: 1,
|
||||
sort: sortByTimeDesc,
|
||||
_source: [TIMESTAMP, getMetricField(metric)],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Builds aggregation to retrieve the most recent log entry with the given status
|
||||
* @deprecated getLastEntryAggregation is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*
|
||||
* @param status - rule execution status
|
||||
* @returns aggregation
|
||||
*/
|
||||
export const getLastEntryAggregation = (status: RuleExecutionStatus) => ({
|
||||
filter: {
|
||||
term: { [RULE_STATUS]: status },
|
||||
},
|
||||
aggs: {
|
||||
event: {
|
||||
top_hits: {
|
||||
sort: sortByTimeDesc,
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObjectsFindResult } from '../../../../../../../src/core/server';
|
||||
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { IRuleStatusSOAttributes } from '../rules/types';
|
||||
|
||||
export enum ExecutionMetric {
|
||||
'executionGap' = 'executionGap',
|
||||
'searchDurationMax' = 'searchDurationMax',
|
||||
'indexingDurationMax' = 'indexingDurationMax',
|
||||
'indexingLookback' = 'indexingLookback',
|
||||
}
|
||||
|
||||
export type ExecutionMetricValue<T extends ExecutionMetric> = {
|
||||
[ExecutionMetric.executionGap]: number;
|
||||
[ExecutionMetric.searchDurationMax]: number;
|
||||
[ExecutionMetric.indexingDurationMax]: number;
|
||||
[ExecutionMetric.indexingLookback]: Date;
|
||||
}[T];
|
||||
|
||||
export interface FindExecutionLogArgs {
|
||||
ruleId: string;
|
||||
spaceId: string;
|
||||
logsCount?: number;
|
||||
}
|
||||
|
||||
export interface FindBulkExecutionLogArgs {
|
||||
ruleIds: string[];
|
||||
spaceId: string;
|
||||
logsCount?: number;
|
||||
}
|
||||
|
||||
export interface LogStatusChangeArgs {
|
||||
ruleId: string;
|
||||
spaceId: string;
|
||||
newStatus: RuleExecutionStatus;
|
||||
namespace?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ExecutionMetricArgs<T extends ExecutionMetric> {
|
||||
ruleId: string;
|
||||
spaceId: string;
|
||||
namespace?: string;
|
||||
metric: T;
|
||||
value: ExecutionMetricValue<T>;
|
||||
}
|
||||
|
||||
export interface FindBulkExecutionLogResponse {
|
||||
[ruleId: string]: IRuleStatusSOAttributes[] | undefined;
|
||||
}
|
||||
|
||||
export interface IRuleExecutionLogClient {
|
||||
find: (
|
||||
args: FindExecutionLogArgs
|
||||
) => Promise<Array<SavedObjectsFindResult<IRuleStatusSOAttributes>>>;
|
||||
findBulk: (args: FindBulkExecutionLogArgs) => Promise<FindBulkExecutionLogResponse>;
|
||||
create: (event: IRuleStatusSOAttributes, spaceId: string) => Promise<void>;
|
||||
update: (id: string, event: IRuleStatusSOAttributes, spaceId: string) => Promise<void>;
|
||||
delete: (id: string) => Promise<void>;
|
||||
// TODO These methods are intended to supersede ones provided by RuleStatusService
|
||||
logStatusChange: (args: LogStatusChangeArgs) => Promise<void>;
|
||||
logExecutionMetric: <T extends ExecutionMetric>(args: ExecutionMetricArgs<T>) => Promise<void>;
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Logger } from '@kbn/logging';
|
||||
import { AlertInstanceContext, AlertTypeParams } from '../../../../../alerting/common';
|
||||
import { AlertTypeWithExecutor, RuleDataPluginService } from '../../../../../rule_registry/server';
|
||||
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { RuleExecutionLogClient } from './rule_execution_log_client';
|
||||
import { IRuleExecutionLogClient } from './types';
|
||||
|
||||
export interface ExecutionLogServices {
|
||||
ruleExecutionLogClient: IRuleExecutionLogClient;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
type WithRuleExecutionLog = (args: {
|
||||
logger: Logger;
|
||||
ruleDataService: RuleDataPluginService;
|
||||
}) => <
|
||||
TParams extends AlertTypeParams,
|
||||
TAlertInstanceContext extends AlertInstanceContext,
|
||||
TServices extends ExecutionLogServices
|
||||
>(
|
||||
type: AlertTypeWithExecutor<TParams, TAlertInstanceContext, TServices>
|
||||
) => AlertTypeWithExecutor<TParams, TAlertInstanceContext, TServices>;
|
||||
|
||||
export const withRuleExecutionLogFactory: WithRuleExecutionLog = ({ logger, ruleDataService }) => (
|
||||
type
|
||||
) => {
|
||||
return {
|
||||
...type,
|
||||
executor: async (options) => {
|
||||
const ruleExecutionLogClient = new RuleExecutionLogClient({
|
||||
ruleDataService,
|
||||
savedObjectsClient: options.services.savedObjectsClient,
|
||||
});
|
||||
try {
|
||||
await ruleExecutionLogClient.logStatusChange({
|
||||
spaceId: options.spaceId,
|
||||
ruleId: options.alertId,
|
||||
newStatus: RuleExecutionStatus['going to run'],
|
||||
});
|
||||
|
||||
const state = await type.executor({
|
||||
...options,
|
||||
services: {
|
||||
...options.services,
|
||||
ruleExecutionLogClient,
|
||||
logger,
|
||||
},
|
||||
});
|
||||
|
||||
await ruleExecutionLogClient.logStatusChange({
|
||||
spaceId: options.spaceId,
|
||||
ruleId: options.alertId,
|
||||
newStatus: RuleExecutionStatus.succeeded,
|
||||
});
|
||||
|
||||
return state;
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
await ruleExecutionLogClient.logStatusChange({
|
||||
spaceId: options.spaceId,
|
||||
ruleId: options.alertId,
|
||||
newStatus: RuleExecutionStatus.failed,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
|
@ -7,25 +7,25 @@
|
|||
|
||||
import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks';
|
||||
import { 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);
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 {};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 & {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue