From 16cd13d7cb58b0f9bf1a9c7a2c01bd454d4234ed Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Fri, 28 May 2021 12:38:49 -0600 Subject: [PATCH] [RAC][Security Solution] Register Security Detection Rules with Rule Registry (#96015) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR starts the migration of the Security Solution rules to use the rule-registry introduced in https://github.com/elastic/kibana/pull/95903. This is a pathfinding effort in porting over the existing Security Solution rules, and may include some temporary reference rules for testing out different paradigms as we move the rules over. See https://github.com/elastic/kibana/issues/95735 for details Enable via the following feature flags in your `kibana.dev.yml`: ``` # Security Solution Rules on Rule Registry xpack.ruleRegistry.index: '.kibana-[USERNAME]-alerts' # Only necessary to scope from other devs testing, if not specified defaults to `.alerts-security-solution` xpack.securitySolution.enableExperimental: ['ruleRegistryEnabled'] ``` > Note: if setting a custom `xpack.ruleRegistry.index`, for the time being you must also update the [DEFAULT_ALERTS_INDEX](https://github.com/elastic/kibana/blob/9e213fb7a5a0337591a50a0567924ebe950b9791/x-pack/plugins/security_solution/common/constants.ts#L28) in order for the UI to display alerts within the alerts table. --- Three reference rule types have been added (`query`, `eql`, `threshold`), along with scripts for creating them located in: ``` x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/ ``` Main Detection page TGrid queries have been short-circuited to query `.alerts-security-solution*` for displaying alerts from the new alerts as data indices. To test, checkout, enable the above feature flag(s), and run one of the scripts from the above directory, e.g. `./create_reference_rule_query.sh` (ensure your ENV vars as set! :) Alerts as data within the main Detection Page 🎉

cc @madirey @dgieselaar @pmuellr @yctercero @dhurley14 @marshallmain # Conflicts: # x-pack/plugins/security_solution/server/plugin.ts --- x-pack/plugins/rule_registry/README.md | 3 + x-pack/plugins/rule_registry/server/index.ts | 1 + .../server/rule_data_client/index.ts | 4 +- .../create_persistence_rule_type_factory.ts | 112 ++++++++++ .../security_solution/common/constants.ts | 13 ++ .../common/experimental_features.ts | 1 + x-pack/plugins/security_solution/kibana.json | 1 + .../exceptions/use_add_exception.test.tsx | 18 +- .../exceptions/use_add_exception.tsx | 20 +- .../public/common/mock/global_state.ts | 1 + .../public/common/mock/test_providers.tsx | 26 ++- .../alerts_table/default_config.tsx | 76 ++++++- .../components/alerts_table/index.tsx | 32 ++- .../alerts/use_signal_index.test.tsx | 31 ++- .../alerts/use_signal_index.tsx | 13 +- .../detection_engine/detection_engine.tsx | 16 +- .../detection_engine/rules/details/index.tsx | 11 +- .../security_solution/public/plugin.tsx | 18 +- .../public/timelines/containers/helpers.ts | 12 +- .../timelines/containers/index.test.tsx | 5 + .../public/timelines/containers/index.tsx | 15 +- .../reference_rules/__mocks__/rule_type.ts | 76 +++++++ .../reference_rules/__mocks__/threshold.ts | 61 ++++++ .../reference_rules/eql.test.ts | 92 ++++++++ .../detection_engine/reference_rules/eql.ts | 121 ++++++++++ .../detection_engine/reference_rules/ml.ts | 70 ++++++ .../reference_rules/query.test.ts | 99 +++++++++ .../detection_engine/reference_rules/query.ts | 88 ++++++++ .../scripts/create_reference_rule_eql.sh | 34 +++ .../scripts/create_reference_rule_query.sh | 34 +++ .../create_reference_rule_threshold.sh | 37 ++++ .../reference_rules/threshold.test.ts | 132 +++++++++++ .../reference_rules/threshold.ts | 206 ++++++++++++++++++ .../routes/index/read_index_route.ts | 16 +- .../routes/rules/create_rules_route.ts | 4 +- .../routes/rules/delete_rules_route.ts | 6 +- .../routes/rules/find_rules_route.ts | 6 +- .../routes/rules/patch_rules_route.ts | 7 +- .../routes/rules/read_rules_route.ts | 6 +- .../routes/rules/update_rules_route.ts | 7 +- .../signals/query_signals_route.test.ts | 4 +- .../routes/signals/query_signals_route.ts | 14 +- .../threshold/find_threshold_signals.ts | 2 +- .../security_solution/server/plugin.ts | 111 +++++++++- .../security_solution/server/routes/index.ts | 23 +- .../factory/hosts/details/index.test.tsx | 1 + 46 files changed, 1606 insertions(+), 80 deletions(-) create mode 100644 x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/rule_type.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/threshold.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/ml.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts create mode 100755 x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_eql.sh create mode 100755 x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_query.sh create mode 100755 x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_threshold.sh create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/threshold.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/threshold.ts diff --git a/x-pack/plugins/rule_registry/README.md b/x-pack/plugins/rule_registry/README.md index cfbde612b45a6d..e12c2b29ed3738 100644 --- a/x-pack/plugins/rule_registry/README.md +++ b/x-pack/plugins/rule_registry/README.md @@ -145,3 +145,6 @@ The following fields are defined in the technical field component template and s - `kibana.rac.alert.severity.value`: the severity of the alert, as a numerical value, which allows sorting. - `kibana.rac.alert.evaluation.value`: The measured (numerical value). - `kibana.rac.alert.threshold.value`: The threshold that was defined (or, in case of multiple thresholds, the one that was exceeded). +- `kibana.rac.alert.ancestors`: the array of ancestors (if any) for the alert. +- `kibana.rac.alert.depth`: the depth of the alert in the ancestral tree (default 0). +- `kibana.rac.alert.building_block_type`: the building block type of the alert (default undefined). diff --git a/x-pack/plugins/rule_registry/server/index.ts b/x-pack/plugins/rule_registry/server/index.ts index 9547f165cd7058..9eefc19f34670e 100644 --- a/x-pack/plugins/rule_registry/server/index.ts +++ b/x-pack/plugins/rule_registry/server/index.ts @@ -14,6 +14,7 @@ export { RuleDataClient } from './rule_data_client'; export { IRuleDataClient } from './rule_data_client/types'; export { getRuleExecutorData, RuleExecutorData } from './utils/get_rule_executor_data'; export { createLifecycleRuleTypeFactory } from './utils/create_lifecycle_rule_type_factory'; +export { createPersistenceRuleTypeFactory } from './utils/create_persistence_rule_type_factory'; export const plugin = (initContext: PluginInitializerContext) => new RuleRegistryPlugin(initContext); diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/index.ts b/x-pack/plugins/rule_registry/server/rule_data_client/index.ts index 135c870f207272..43122ba49519a4 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/index.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/index.ts @@ -73,8 +73,8 @@ export class RuleDataClient implements IRuleDataClient { return clusterClient.bulk(requestWithDefaultParameters).then((response) => { if (response.body.errors) { if ( - response.body.items.length === 1 && - response.body.items[0]?.index?.error?.type === 'index_not_found_exception' + response.body.items.length > 0 && + response.body.items?.[0]?.index?.error?.type === 'index_not_found_exception' ) { return this.createOrUpdateWriteTarget({ namespace }).then(() => { return clusterClient.bulk(requestWithDefaultParameters); diff --git a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts new file mode 100644 index 00000000000000..0e244fbaa2ee35 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts @@ -0,0 +1,112 @@ +/* + * 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 { ESSearchRequest } from 'typings/elasticsearch'; +import v4 from 'uuid/v4'; +import { Logger } from '@kbn/logging'; + +import { AlertInstance } from '../../../alerting/server'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertTypeParams, +} from '../../../alerting/common'; +import { RuleDataClient } from '../rule_data_client'; +import { AlertTypeWithExecutor } from '../types'; + +type PersistenceAlertService> = ( + alerts: Array> +) => Array>; + +type PersistenceAlertQueryService = ( + query: ESSearchRequest +) => Promise>>; + +type CreatePersistenceRuleTypeFactory = (options: { + ruleDataClient: RuleDataClient; + logger: Logger; +}) => < + TParams extends AlertTypeParams, + TAlertInstanceContext extends AlertInstanceContext, + TServices extends { + alertWithPersistence: PersistenceAlertService; + findAlerts: PersistenceAlertQueryService; + } +>( + type: AlertTypeWithExecutor +) => AlertTypeWithExecutor; + +export const createPersistenceRuleTypeFactory: CreatePersistenceRuleTypeFactory = ({ + logger, + ruleDataClient, +}) => (type) => { + return { + ...type, + executor: async (options) => { + const { + services: { alertInstanceFactory, scopedClusterClient }, + } = options; + + const currentAlerts: Array> = []; + const timestamp = options.startedAt.toISOString(); + + const state = await type.executor({ + ...options, + services: { + ...options.services, + alertWithPersistence: (alerts) => { + alerts.forEach((alert) => currentAlerts.push(alert)); + return alerts.map((alert) => + alertInstanceFactory(alert['kibana.rac.alert.uuid']! as string) + ); + }, + findAlerts: async (query) => { + const { body } = await scopedClusterClient.asCurrentUser.search({ + ...query, + body: { + ...query.body, + }, + ignore_unavailable: true, + }); + return body.hits.hits + .map((event: { _source: any }) => event._source!) + .map((event: { [x: string]: any }) => { + const alertUuid = event['kibana.rac.alert.uuid']; + const isAlert = alertUuid != null; + return { + ...event, + 'event.kind': 'signal', + 'kibana.rac.alert.id': '???', + 'kibana.rac.alert.status': 'open', + 'kibana.rac.alert.uuid': v4(), + 'kibana.rac.alert.ancestors': isAlert + ? ((event['kibana.rac.alert.ancestors'] as string[]) ?? []).concat([ + alertUuid!, + ] as string[]) + : [], + 'kibana.rac.alert.depth': isAlert + ? ((event['kibana.rac.alert.depth'] as number) ?? 0) + 1 + : 0, + '@timestamp': timestamp, + }; + }); + }, + }, + }); + + const numAlerts = currentAlerts.length; + logger.debug(`Found ${numAlerts} alerts.`); + + if (ruleDataClient && numAlerts) { + await ruleDataClient.getWriter().bulk({ + body: currentAlerts.flatMap((event) => [{ index: {} }, event]), + }); + } + + return state; + }, + }; +}; diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index effefdd438c5cc..91b48afdc4ed1e 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -25,6 +25,7 @@ export const DEFAULT_TIME_RANGE = 'timepicker:timeDefaults'; export const DEFAULT_REFRESH_RATE_INTERVAL = 'timepicker:refreshIntervalDefaults'; export const DEFAULT_APP_TIME_RANGE = 'securitySolution:timeDefaults'; export const DEFAULT_APP_REFRESH_INTERVAL = 'securitySolution:refreshIntervalDefaults'; +export const DEFAULT_ALERTS_INDEX = '.alerts-security-solution'; export const DEFAULT_SIGNALS_INDEX = '.siem-signals'; export const DEFAULT_LISTS_INDEX = '.lists'; export const DEFAULT_ITEMS_INDEX = '.items'; @@ -148,6 +149,18 @@ export const DEFAULT_TRANSFORMS_SETTING = JSON.stringify(defaultTransformsSettin */ export const SIGNALS_ID = `siem.signals`; +/** + * Id's for reference rule types + */ +export const REFERENCE_RULE_ALERT_TYPE_ID = `siem.referenceRule`; +export const REFERENCE_RULE_PERSISTENCE_ALERT_TYPE_ID = `siem.referenceRulePersistence`; + +export const CUSTOM_ALERT_TYPE_ID = `siem.customRule`; +export const EQL_ALERT_TYPE_ID = `siem.eqlRule`; +export const INDICATOR_ALERT_TYPE_ID = `siem.indicatorRule`; +export const ML_ALERT_TYPE_ID = `siem.mlRule`; +export const THRESHOLD_ALERT_TYPE_ID = `siem.thresholdRule`; + /** * Id for the notifications alerting type */ diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 6195dd61a79841..02006fdb29d475 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -15,6 +15,7 @@ const allowedExperimentalValues = Object.freeze({ trustedAppsByPolicyEnabled: false, metricsEntitiesEnabled: false, hostIsolationEnabled: false, + ruleRegistryEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index 50a5f62740271f..02dbc56bd33976 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -8,6 +8,7 @@ "actions", "alerting", "cases", + "ruleRegistry", "data", "dataEnhanced", "embeddable", diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index 3d29650b750dc9..e4a015525dfb44 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -21,7 +21,7 @@ import type { CreateExceptionListItemSchema, UpdateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; - +import { TestProviders } from '../../mock'; import { useAddOrUpdateException, UseAddOrUpdateExceptionProps, @@ -134,12 +134,16 @@ describe('useAddOrUpdateException', () => { addOrUpdateItemsArgs = [ruleId, itemsToAddOrUpdate]; render = () => - renderHook(() => - useAddOrUpdateException({ - http: mockKibanaHttpService, - onError, - onSuccess, - }) + renderHook( + () => + useAddOrUpdateException({ + http: mockKibanaHttpService, + onError, + onSuccess, + }), + { + wrapper: TestProviders, + } ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx index 5ba73ba2c90588..dbae0964b41a8c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx @@ -19,9 +19,11 @@ import { getUpdateAlertsQuery } from '../../../detections/components/alerts_tabl import { buildAlertStatusFilter, buildAlertsRuleIdFilter, + buildAlertStatusFilterRuleRegistry, } from '../../../detections/components/alerts_table/default_config'; import { getQueryFilter } from '../../../../common/detection_engine/get_query_filter'; import { Index } from '../../../../common/detection_engine/schemas/common/schemas'; +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { formatExceptionItemForUpdate, prepareExceptionItemsForBulkClose } from './helpers'; import { useKibana } from '../../lib/kibana'; @@ -82,6 +84,8 @@ export const useAddOrUpdateException = ({ }, [] ); + // TODO: Once we are past experimental phase this code should be removed + const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); useEffect(() => { let isSubscribed = true; @@ -127,10 +131,15 @@ export const useAddOrUpdateException = ({ } if (bulkCloseIndex != null) { + // TODO: Once we are past experimental phase this code should be removed + const alertStatusFilter = ruleRegistryEnabled + ? buildAlertStatusFilterRuleRegistry('open') + : buildAlertStatusFilter('open'); + const filter = getQueryFilter( '', 'kuery', - [...buildAlertsRuleIdFilter(ruleId), ...buildAlertStatusFilter('open')], + [...buildAlertsRuleIdFilter(ruleId), ...alertStatusFilter], bulkCloseIndex, prepareExceptionItemsForBulkClose(exceptionItemsToAddOrUpdate), false @@ -176,7 +185,14 @@ export const useAddOrUpdateException = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [http, onSuccess, onError, updateExceptionListItem, addExceptionListItem]); + }, [ + addExceptionListItem, + http, + onSuccess, + onError, + ruleRegistryEnabled, + updateExceptionListItem, + ]); return [{ isLoading }, addOrUpdateException]; }; diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index af278b09e719c4..71e33c603b65b2 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -43,6 +43,7 @@ export const mockGlobalState: State = { trustedAppsByPolicyEnabled: false, metricsEntitiesEnabled: false, hostIsolationEnabled: false, + ruleRegistryEnabled: false, }, }, hosts: { diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 90526e84a2262b..9ac7ae0f243225 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -24,11 +24,12 @@ import { import { FieldHook } from '../../shared_imports'; import { SUB_PLUGINS_REDUCER } from './utils'; import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage'; +import { UserPrivilegesProvider } from '../../detections/components/user_privileges'; const state: State = mockGlobalState; interface Props { - children: React.ReactNode; + children?: React.ReactNode; store?: Store; onDragEnd?: (result: DropResult, provided: ResponderProvided) => void; } @@ -59,7 +60,30 @@ const TestProvidersComponent: React.FC = ({ ); +/** + * A utility for wrapping children in the providers required to run most tests + * WITH user privileges provider. + */ +const TestProvidersWithPrivilegesComponent: React.FC = ({ + children, + store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage), + onDragEnd = jest.fn(), +}) => ( + + + + ({ eui: euiDarkVars, darkMode: true })}> + + {children} + + + + + +); + export const TestProviders = React.memo(TestProvidersComponent); +export const TestProvidersWithPrivileges = React.memo(TestProvidersWithPrivilegesComponent); export const useFormFieldMock = (options?: Partial>): FieldHook => { return { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 478c8930b8dd3b..02a815bc59f3bd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -5,11 +5,12 @@ * 2.0. */ +import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { RowRendererId } from '../../../../common/types/timeline'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; -import { SubsetTimelineModel } from '../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { columns } from '../../configurations/security_solution_detections/columns'; @@ -124,3 +125,76 @@ export const requiredFieldsForActions = [ 'host.os.family', 'event.code', ]; + +// TODO: Once we are past experimental phase this code should be removed +export const buildAlertStatusFilterRuleRegistry = (status: Status): Filter[] => [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'kibana.rac.alert.status', + params: { + query: status, + }, + }, + query: { + term: { + 'kibana.rac.alert.status': status, + }, + }, + }, +]; + +export const buildShowBuildingBlockFilterRuleRegistry = ( + showBuildingBlockAlerts: boolean +): Filter[] => + showBuildingBlockAlerts + ? [] + : [ + { + meta: { + alias: null, + negate: true, + disabled: false, + type: 'exists', + key: 'kibana.rac.rule.building_block_type', + value: 'exists', + }, + // @ts-expect-error TODO: Rework parent typings to support ExistsFilter[] + exists: { field: 'kibana.rac.rule.building_block_type' }, + }, + ]; + +export const requiredFieldMappingsForActionsRuleRegistry = { + '@timestamp': '@timestamp', + 'alert.id': 'kibana.rac.alert.id', + 'event.kind': 'event.kind', + 'alert.start': 'kibana.rac.alert.start', + 'alert.uuid': 'kibana.rac.alert.uuid', + 'event.action': 'event.action', + 'alert.status': 'kibana.rac.alert.status', + 'alert.duration.us': 'kibana.rac.alert.duration.us', + 'rule.uuid': 'rule.uuid', + 'rule.id': 'rule.id', + 'rule.name': 'rule.name', + 'rule.category': 'rule.category', + producer: 'kibana.rac.alert.producer', + tags: 'tags', +}; + +export const alertsHeadersRuleRegistry: ColumnHeaderOptions[] = Object.entries( + requiredFieldMappingsForActionsRuleRegistry +).map(([alias, field]) => ({ + columnHeaderType: defaultColumnHeaderType, + displayAsText: alias, + id: field, +})); + +export const alertsDefaultModelRuleRegistry: SubsetTimelineModel = { + ...timelineDefaults, + columns: alertsHeadersRuleRegistry, + showCheckboxes: true, + excludedRowRendererIds: Object.values(RowRendererId), +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 9dc83d7898963d..f20754fc446d6e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -16,6 +16,7 @@ import { TimelineIdLiteral } from '../../../../common/types/timeline'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; import { HeaderSection } from '../../../common/components/header_section'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { combineQueries } from '../../../timelines/components/timeline/helpers'; import { useKibana } from '../../../common/lib/kibana'; import { inputsSelectors, State, inputsModel } from '../../../common/store'; @@ -29,6 +30,8 @@ import { requiredFieldsForActions, alertsDefaultModel, buildAlertStatusFilter, + alertsDefaultModelRuleRegistry, + buildAlertStatusFilterRuleRegistry, } from './default_config'; import { FILTER_OPEN, AlertsTableFilterGroup } from './alerts_filter_group'; import { AlertsUtilityBar } from './alerts_utility_bar'; @@ -104,6 +107,8 @@ export const AlertsTableComponent: React.FC = ({ const [, dispatchToaster] = useStateToaster(); const { addWarning } = useAppToasts(); const { initializeTimeline, setSelectAll } = useManageTimeline(); + // TODO: Once we are past experimental phase this code should be removed + const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); const getGlobalQuery = useCallback( (customFilters: Filter[]) => { @@ -236,7 +241,11 @@ export const AlertsTableComponent: React.FC = ({ refetchQuery: inputsModel.Refetch, { status, selectedStatus }: UpdateAlertsStatusProps ) => { - const currentStatusFilter = buildAlertStatusFilter(status); + // TODO: Once we are past experimental phase this code should be removed + const currentStatusFilter = ruleRegistryEnabled + ? buildAlertStatusFilterRuleRegistry(status) + : buildAlertStatusFilter(status); + await updateAlertStatusAction({ query: showClearSelectionAction ? getGlobalQuery(currentStatusFilter)?.filterQuery @@ -258,6 +267,7 @@ export const AlertsTableComponent: React.FC = ({ showClearSelectionAction, onAlertStatusUpdateSuccess, onAlertStatusUpdateFailure, + ruleRegistryEnabled, ] ); @@ -301,18 +311,28 @@ export const AlertsTableComponent: React.FC = ({ ); const defaultFiltersMemo = useMemo(() => { + // TODO: Once we are past experimental phase this code should be removed + const alertStatusFilter = ruleRegistryEnabled + ? buildAlertStatusFilterRuleRegistry(filterGroup) + : buildAlertStatusFilter(filterGroup); + if (isEmpty(defaultFilters)) { - return buildAlertStatusFilter(filterGroup); + return alertStatusFilter; } else if (defaultFilters != null && !isEmpty(defaultFilters)) { - return [...defaultFilters, ...buildAlertStatusFilter(filterGroup)]; + return [...defaultFilters, ...alertStatusFilter]; } - }, [defaultFilters, filterGroup]); + }, [defaultFilters, filterGroup, ruleRegistryEnabled]); const { filterManager } = useKibana().services.data.query; + // TODO: Once we are past experimental phase this code should be removed + const defaultTimelineModel = ruleRegistryEnabled + ? alertsDefaultModelRuleRegistry + : alertsDefaultModel; + useEffect(() => { initializeTimeline({ defaultModel: { - ...alertsDefaultModel, + ...defaultTimelineModel, columns, }, documentType: i18n.ALERTS_DOCUMENT_TYPE, @@ -344,7 +364,7 @@ export const AlertsTableComponent: React.FC = ({ return ( ( - {children} -); - describe('useSignalIndex', () => { let appToastsMock: jest.Mocked>; @@ -33,7 +28,9 @@ describe('useSignalIndex', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => useSignalIndex(), - { wrapper: Wrapper } + { + wrapper: TestProvidersWithPrivileges, + } ); await waitForNextUpdate(); expect(result.current).toEqual({ @@ -50,7 +47,9 @@ describe('useSignalIndex', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => useSignalIndex(), - { wrapper: Wrapper } + { + wrapper: TestProvidersWithPrivileges, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -69,7 +68,9 @@ describe('useSignalIndex', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => useSignalIndex(), - { wrapper: Wrapper } + { + wrapper: TestProvidersWithPrivileges, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -93,7 +94,9 @@ describe('useSignalIndex', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => useSignalIndex(), - { wrapper: Wrapper } + { + wrapper: TestProvidersWithPrivileges, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -114,7 +117,9 @@ describe('useSignalIndex', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => useSignalIndex(), - { wrapper: Wrapper } + { + wrapper: TestProvidersWithPrivileges, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -140,7 +145,9 @@ describe('useSignalIndex', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => useSignalIndex(), - { wrapper: Wrapper } + { + wrapper: TestProvidersWithPrivileges, + } ); await waitForNextUpdate(); await waitForNextUpdate(); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx index fdbeab26f11f3a..84eaf8e3aa93c3 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx @@ -6,8 +6,10 @@ */ import { useEffect, useState } from 'react'; +import { DEFAULT_ALERTS_INDEX } from '../../../../../common/constants'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { createSignalIndex, getSignalIndex } from './api'; import * as i18n from './translations'; import { isSecurityAppError } from '../../../../common/utils/api'; @@ -38,6 +40,8 @@ export const useSignalIndex = (): ReturnSignalIndex => { }); const { addError } = useAppToasts(); const { hasIndexRead } = useAlertsPrivileges(); + // TODO: Once we are past experimental phase this code should be removed + const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); useEffect(() => { let isSubscribed = true; @@ -48,10 +52,15 @@ export const useSignalIndex = (): ReturnSignalIndex => { setLoading(true); const signal = await getSignalIndex({ signal: abortCtrl.signal }); + // TODO: Once we are past experimental phase we can update `getSignalIndex` to return the space-aware DEFAULT_ALERTS_INDEX + const signalIndices = ruleRegistryEnabled + ? `${DEFAULT_ALERTS_INDEX},${signal.name}` + : signal.name; + if (isSubscribed && signal != null) { setSignalIndex({ signalIndexExists: true, - signalIndexName: signal.name, + signalIndexName: signalIndices, signalIndexMappingOutdated: signal.index_mapping_outdated, createDeSignalIndex: createIndex, }); @@ -115,7 +124,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { isSubscribed = false; abortCtrl.abort(); }; - }, [addError, hasIndexRead]); + }, [addError, hasIndexRead, ruleRegistryEnabled]); return { loading, ...signalIndex }; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index c1c7e4688bbbe3..8ae7e4fb2852b5 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -11,6 +11,7 @@ import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; import { SecurityPageName } from '../../../app/types'; @@ -51,6 +52,7 @@ import { timelineSelectors } from '../../../timelines/store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { buildShowBuildingBlockFilter, + buildShowBuildingBlockFilterRuleRegistry, buildThreatMatchFilter, } from '../../components/alerts_table/default_config'; import { useSourcererScope } from '../../../common/containers/sourcerer'; @@ -81,6 +83,8 @@ const DetectionEnginePageComponent = () => { const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); const query = useDeepEqualSelector(getGlobalQuerySelector); const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); + // TODO: Once we are past experimental phase this code should be removed + const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); const { to, from, deleteQuery, setQuery } = useGlobalTime(); const { globalFullScreen } = useGlobalFullScreen(); @@ -134,19 +138,23 @@ const DetectionEnginePageComponent = () => { const alertsHistogramDefaultFilters = useMemo( () => [ ...filters, - ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ...(ruleRegistryEnabled + ? buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts) // TODO: Once we are past experimental phase this code should be removed + : buildShowBuildingBlockFilter(showBuildingBlockAlerts)), ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), ], - [filters, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] + [filters, ruleRegistryEnabled, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] ); // AlertsTable manages global filters itself, so not including `filters` const alertsTableDefaultFilters = useMemo( () => [ - ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ...(ruleRegistryEnabled + ? buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts) // TODO: Once we are past experimental phase this code should be removed + : buildShowBuildingBlockFilter(showBuildingBlockAlerts)), ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), ], - [showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] + [ruleRegistryEnabled, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] ); const onShowBuildingBlockAlertsChangedCallback = useCallback( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index d3793dad8ff1a4..8dac9e03514d1c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -36,6 +36,7 @@ import { useDeepEqualSelector, useShallowEqualSelector, } from '../../../../../common/hooks/use_selector'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { useKibana } from '../../../../../common/lib/kibana'; import { TimelineId } from '../../../../../../common/types/timeline'; import { UpdateDateRange } from '../../../../../common/components/charts/common'; @@ -64,6 +65,7 @@ import { StepScheduleRule } from '../../../../components/rules/step_schedule_rul import { buildAlertsRuleIdFilter, buildShowBuildingBlockFilter, + buildShowBuildingBlockFilterRuleRegistry, buildThreatMatchFilter, } from '../../../../components/alerts_table/default_config'; import { RuleSwitch } from '../../../../components/rules/rule_switch'; @@ -222,6 +224,9 @@ const RuleDetailsPageComponent = () => { const { formatUrl } = useFormatUrl(SecurityPageName.detections); const { globalFullScreen } = useGlobalFullScreen(); + // TODO: Once we are past experimental phase this code should be removed + const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); + // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities); const { @@ -307,10 +312,12 @@ const RuleDetailsPageComponent = () => { const alertDefaultFilters = useMemo( () => [ ...buildAlertsRuleIdFilter(ruleId), - ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ...(ruleRegistryEnabled + ? buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts) // TODO: Once we are past experimental phase this code should be removed + : buildShowBuildingBlockFilter(showBuildingBlockAlerts)), ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), ], - [ruleId, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] + [ruleId, ruleRegistryEnabled, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] ); const alertMergedFilters = useMemo(() => [...alertDefaultFilters, ...filters], [ diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index c1f501d3f70945..2e41e291156aaa 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -44,6 +44,7 @@ import { APP_PATH, DEFAULT_INDEX_KEY, DETECTION_ENGINE_INDEX_URL, + DEFAULT_ALERTS_INDEX, } from '../common/constants'; import { SecurityPageName } from './app/types'; @@ -446,6 +447,9 @@ export class Plugin implements IPlugin { if (!this._store) { + const experimentalFeatures = parseExperimentalConfigValue( + this.config.enableExperimental || [] + ); const defaultIndicesName = coreStart.uiSettings.get(DEFAULT_INDEX_KEY); const [ { createStore, createInitialState }, @@ -474,9 +478,15 @@ export class Plugin implements IPlugin +// TODO: Once we are past experimental phase `useRuleRegistry` should be removed +export const skipQueryForDetectionsPage = ( + id: string, + defaultIndex: string[], + useRuleRegistry = false +) => id != null && detectionsTimelineIds.some((timelineId) => timelineId === id) && - !defaultIndex.some((di) => di.toLowerCase().startsWith('.siem-signals')); + !defaultIndex.some((di) => + di.toLowerCase().startsWith(useRuleRegistry ? DEFAULT_ALERTS_INDEX : '.siem-signals') + ); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx index 1032d0ec1672ac..62846eb01e60f6 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx @@ -9,6 +9,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { initSortDefault, TimelineArgs, useTimelineEvents, UseTimelineEventsProps } from '.'; import { SecurityPageName } from '../../../common/constants'; import { TimelineId } from '../../../common/types/timeline'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { mockTimelineData } from '../../common/mock'; import { useRouteSpy } from '../../common/utils/route/use_route_spy'; @@ -26,6 +27,9 @@ const mockEvents = mockTimelineData.filter((i, index) => index <= 11); const mockSearch = jest.fn(); +jest.mock('../../common/hooks/use_experimental_features'); +const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; + jest.mock('../../common/lib/kibana', () => ({ useToasts: jest.fn().mockReturnValue({ addError: jest.fn(), @@ -93,6 +97,7 @@ mockUseRouteSpy.mockReturnValue([ ]); describe('useTimelineEvents', () => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(false); beforeEach(() => { mockSearch.mockReset(); }); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 92199336b978c1..17c107899d85ab 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -13,6 +13,7 @@ import { Subscription } from 'rxjs'; import { ESQuery } from '../../../common/typed_json'; import { isCompleteResponse, isErrorResponse } from '../../../../../../src/plugins/data/public'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { inputsModel, KueryFilterQueryKind } from '../../common/store'; import { useKibana } from '../../common/lib/kibana'; import { createFilter } from '../../common/containers/helpers'; @@ -197,6 +198,9 @@ export const useTimelineEvents = ({ }); const { addError, addWarning } = useAppToasts(); + // TODO: Once we are past experimental phase this code should be removed + const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); + const timelineSearch = useCallback( (request: TimelineRequest | null) => { if (request == null || pageName === '' || skip) { @@ -305,7 +309,10 @@ export const useTimelineEvents = ({ ); useEffect(() => { - if (skipQueryForDetectionsPage(id, indexNames) || indexNames.length === 0) { + if ( + skipQueryForDetectionsPage(id, indexNames, ruleRegistryEnabled) || + indexNames.length === 0 + ) { return; } @@ -364,7 +371,10 @@ export const useTimelineEvents = ({ activeTimeline.setActivePage(newActivePage); } } - if (!skipQueryForDetectionsPage(id, indexNames) && !deepEqual(prevRequest, currentRequest)) { + if ( + !skipQueryForDetectionsPage(id, indexNames, ruleRegistryEnabled) && + !deepEqual(prevRequest, currentRequest) + ) { return currentRequest; } return prevRequest; @@ -380,6 +390,7 @@ export const useTimelineEvents = ({ id, language, limit, + ruleRegistryEnabled, startDate, sort, fields, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/rule_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/rule_type.ts new file mode 100644 index 00000000000000..f7e0dd9eb36205 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/rule_type.ts @@ -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 { of } from 'rxjs'; +import { v4 } from 'uuid'; + +import { Logger } from 'kibana/server'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; + +import type { RuleDataClient } from '../../../../../../rule_registry/server'; +import { PluginSetupContract as AlertingPluginSetupContract } from '../../../../../../alerting/server'; +import { ConfigType } from '../../../../config'; + +export const createRuleTypeMocks = () => { + /* eslint-disable @typescript-eslint/no-explicit-any */ + let alertExecutor: (...args: any[]) => Promise; + + const mockedConfig$ = of({} as ConfigType); + + const loggerMock = ({ + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + } as unknown) as Logger; + + const alerting = { + registerType: ({ executor }) => { + alertExecutor = executor; + }, + } as AlertingPluginSetupContract; + + const scheduleActions = jest.fn(); + + const services = { + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + findAlerts: jest.fn(), // TODO: does this stay? + alertWithPersistence: jest.fn(), + logger: loggerMock, + }; + + return { + dependencies: { + alerting, + config$: mockedConfig$, + logger: loggerMock, + ruleDataClient: ({ + getReader: () => { + return { + search: jest.fn(), + }; + }, + getWriter: () => { + return { + bulk: jest.fn(), + }; + }, + } as unknown) as RuleDataClient, + }, + services, + scheduleActions, + executor: async ({ params }: { params: Record }) => { + return alertExecutor({ + services, + params, + alertId: v4(), + startedAt: new Date(), + }); + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/threshold.ts new file mode 100644 index 00000000000000..40d2ed37a55769 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/threshold.ts @@ -0,0 +1,61 @@ +/* + * 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 { sampleDocNoSortId } from '../../signals/__mocks__/es_results'; + +export const mockThresholdResults = { + rawResponse: { + body: { + is_partial: false, + is_running: false, + took: 527, + timed_out: false, + hits: { + total: { + value: 0, + relation: 'eq', + }, + hits: [], + }, + aggregations: { + 'threshold_0:source.ip': { + buckets: [ + { + key: '127.0.0.1', + doc_count: 5, + 'threshold_1:host.name': { + buckets: [ + { + key: 'tardigrade', + doc_count: 3, + top_threshold_hits: { + hits: { + total: { + value: 1, + relation: 'eq', + }, + hits: [ + { + ...sampleDocNoSortId(), + 'host.name': 'tardigrade', + }, + ], + }, + }, + cardinality_count: { + value: 3, + }, + }, + ], + }, + }, + ], + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts new file mode 100644 index 00000000000000..6529c594dd5a51 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts @@ -0,0 +1,92 @@ +/* + * 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. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + +import { sequenceResponse } from '../../../search_strategy/timeline/eql/__mocks__'; + +import { createEqlAlertType } from './eql'; +import { createRuleTypeMocks } from './__mocks__/rule_type'; + +describe('EQL alerts', () => { + it('does not send an alert when sequence not found', async () => { + const { services, dependencies, executor } = createRuleTypeMocks(); + const eqlAlertType = createEqlAlertType(dependencies.ruleDataClient, dependencies.logger); + + dependencies.alerting.registerType(eqlAlertType); + + const params = { + eqlQuery: 'sequence by host.name↵[any where true]↵[any where true]↵[any where true]', + indexPatterns: ['*'], + }; + + services.scopedClusterClient.asCurrentUser.transport.request.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [], + sequences: [], + events: [], + total: { + relation: 'eq', + value: 0, + }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + }) + ); + + await executor({ params }); + expect(services.alertInstanceFactory).not.toBeCalled(); + }); + + it('sends a properly formatted alert when sequence is found', async () => { + const { services, dependencies, executor } = createRuleTypeMocks(); + const eqlAlertType = createEqlAlertType(dependencies.ruleDataClient, dependencies.logger); + + dependencies.alerting.registerType(eqlAlertType); + + const params = { + eqlQuery: 'sequence by host.name↵[any where true]↵[any where true]↵[any where true]', + indexPatterns: ['*'], + }; + + services.scopedClusterClient.asCurrentUser.transport.request.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: sequenceResponse.rawResponse.body.hits, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + }) + ); + + await executor({ params }); + expect(services.alertInstanceFactory).toBeCalled(); + /* + expect(services.alertWithPersistence).toBeCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + 'event.kind': 'signal', + 'kibana.rac.alert.building_block_type': 'default', + }), + ]) + ); + */ + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.ts new file mode 100644 index 00000000000000..39d02c808d09e5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.ts @@ -0,0 +1,121 @@ +/* + * 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 moment from 'moment'; +import v4 from 'uuid/v4'; + +import { ApiResponse } from '@elastic/elasticsearch'; +import { schema } from '@kbn/config-schema'; +import { Logger } from '@kbn/logging'; + +import { + RuleDataClient, + createPersistenceRuleTypeFactory, +} from '../../../../../rule_registry/server'; +import { EQL_ALERT_TYPE_ID } from '../../../../common/constants'; +import { buildEqlSearchRequest } from '../../../../common/detection_engine/get_query_filter'; +import { BaseSignalHit, EqlSignalSearchResponse } from '../signals/types'; + +export const createEqlAlertType = (ruleDataClient: RuleDataClient, logger: Logger) => { + const createPersistenceRuleType = createPersistenceRuleTypeFactory({ + ruleDataClient, + logger, + }); + return createPersistenceRuleType({ + id: EQL_ALERT_TYPE_ID, + name: 'EQL Rule', + validate: { + params: schema.object({ + eqlQuery: schema.string(), + indexPatterns: schema.arrayOf(schema.string()), + }), + }, + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + actionVariables: { + context: [{ name: 'server', description: 'the server' }], + }, + minimumLicenseRequired: 'basic', + producer: 'security-solution', + async executor({ + startedAt, + services: { alertWithPersistence, findAlerts, scopedClusterClient }, + params: { indexPatterns, eqlQuery }, + }) { + const from = moment(startedAt).subtract(moment.duration(5, 'm')).toISOString(); // hardcoded 5-minute rule interval + const to = startedAt.toISOString(); + + const request = buildEqlSearchRequest( + eqlQuery, + indexPatterns, + from, + to, + 10, + undefined, + [], + undefined + ); + const { body: response } = (await scopedClusterClient.asCurrentUser.transport.request( + request + )) as ApiResponse; + + const buildSignalFromEvent = (event: BaseSignalHit) => { + return { + ...event, + 'event.kind': 'signal', + 'kibana.rac.alert.id': '???', + 'kibana.rac.alert.uuid': v4(), + '@timestamp': new Date().toISOString(), + }; + }; + + /* eslint-disable @typescript-eslint/no-explicit-any */ + let alerts: any[] = []; + if (response.hits.sequences !== undefined) { + alerts = response.hits.sequences.reduce((allAlerts: any[], sequence) => { + let previousAlertUuid: string | undefined; + return [ + ...allAlerts, + ...sequence.events.map((event, idx) => { + const alert = { + ...buildSignalFromEvent(event), + 'kibana.rac.alert.ancestors': previousAlertUuid != null ? [previousAlertUuid] : [], + 'kibana.rac.alert.building_block_type': 'default', + 'kibana.rac.alert.depth': idx, + }; + previousAlertUuid = alert['kibana.rac.alert.uuid']; + return alert; + }), + ]; + }, []); + } else if (response.hits.events !== undefined) { + alerts = response.hits.events.map((event) => { + return buildSignalFromEvent(event); + }, []); + } else { + throw new Error( + 'eql query response should have either `sequences` or `events` but had neither' + ); + } + + if (alerts.length > 0) { + alertWithPersistence(alerts).forEach((alert) => { + alert.scheduleActions('default', { server: 'server-test' }); + }); + } + + return { + lastChecked: new Date(), + }; + }, + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/ml.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/ml.ts new file mode 100644 index 00000000000000..c07d0436cc90d0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/ml.ts @@ -0,0 +1,70 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { KibanaRequest, Logger } from 'src/core/server'; +import { SavedObject } from 'src/core/types'; + +import { buildEsQuery, IIndexPattern } from '../../../../../../../src/plugins/data/common'; + +import { createPersistenceRuleTypeFactory } from '../../../../../rule_registry/server'; +import { ML_ALERT_TYPE_ID } from '../../../../common/constants'; +import { SecurityRuleRegistry } from '../../../plugin'; + +const createSecurityMlRuleType = createPersistenceRuleTypeFactory(); + +import { + AlertInstanceContext, + AlertInstanceState, + AlertServices, +} from '../../../../../alerting/server'; +import { ListClient } from '../../../../../lists/server'; +import { isJobStarted } from '../../../../common/machine_learning/helpers'; +import { ExceptionListItemSchema } from '../../../../common/shared_imports'; +import { SetupPlugins } from '../../../plugin'; +import { RefreshTypes } from '../types'; +import { bulkCreateMlSignals } from '../signals/bulk_create_ml_signals'; +import { filterEventsAgainstList } from '../signals/filters/filter_events_against_list'; +import { findMlSignals } from '../signals/find_ml_signals'; +import { BuildRuleMessage } from '../signals/rule_messages'; +import { RuleStatusService } from '../signals/rule_status_service'; +import { MachineLearningRuleAttributes } from '../signals/types'; +import { createErrorsFromShard, createSearchAfterReturnType, mergeReturns } from '../signals/utils'; + +export const mlAlertType = createSecurityMlRuleType({ + id: ML_ALERT_TYPE_ID, + name: 'Machine Learning Rule', + validate: { + params: schema.object({ + indexPatterns: schema.arrayOf(schema.string()), + customQuery: schema.string(), + }), + }, + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + actionVariables: { + context: [{ name: 'server', description: 'the server' }], + }, + minimumLicenseRequired: 'basic', + producer: 'security-solution', + async executor({ + services: { alertWithPersistence, findAlerts }, + params: { indexPatterns, customQuery }, + }) { + return { + lastChecked: new Date(), + }; + }, +}); +*/ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.test.ts new file mode 100644 index 00000000000000..e8c45e9ab70568 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.test.ts @@ -0,0 +1,99 @@ +/* + * 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 { v4 } from 'uuid'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + +import { sampleDocNoSortId } from '../signals/__mocks__/es_results'; + +import { createQueryAlertType } from './query'; +import { createRuleTypeMocks } from './__mocks__/rule_type'; + +describe('Custom query alerts', () => { + it('does not send an alert when no events found', async () => { + const { services, dependencies, executor } = createRuleTypeMocks(); + const queryAlertType = createQueryAlertType(dependencies.ruleDataClient, dependencies.logger); + + dependencies.alerting.registerType(queryAlertType); + + const params = { + customQuery: 'dne:42', + indexPatterns: ['*'], + }; + + services.scopedClusterClient.asCurrentUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [], + sequences: [], + events: [], + total: { + relation: 'eq', + value: 0, + }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + }) + ); + + await executor({ params }); + expect(services.alertInstanceFactory).not.toBeCalled(); + }); + + it('sends a properly formatted alert when events are found', async () => { + const { services, dependencies, executor } = createRuleTypeMocks(); + const queryAlertType = createQueryAlertType(dependencies.ruleDataClient, dependencies.logger); + + dependencies.alerting.registerType(queryAlertType); + + const params = { + customQuery: '*:*', + indexPatterns: ['*'], + }; + + services.scopedClusterClient.asCurrentUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [sampleDocNoSortId(v4()), sampleDocNoSortId(v4()), sampleDocNoSortId(v4())], + total: { + relation: 'eq', + value: 3, + }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + }) + ); + + await executor({ params }); + expect(services.alertInstanceFactory).toBeCalled(); + /* + expect(services.alertWithPersistence).toBeCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + 'event.kind': 'signal', + }), + ]) + ); + */ + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts new file mode 100644 index 00000000000000..3911dcabc34de8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts @@ -0,0 +1,88 @@ +/* + * 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 { QueryContainer } from '@elastic/elasticsearch/api/types'; +import { schema } from '@kbn/config-schema'; +import { Logger } from '@kbn/logging'; +import { ESSearchRequest } from 'typings/elasticsearch'; + +import { buildEsQuery, IIndexPattern } from '../../../../../../../src/plugins/data/common'; + +import { + RuleDataClient, + createPersistenceRuleTypeFactory, +} from '../../../../../rule_registry/server'; +import { CUSTOM_ALERT_TYPE_ID } from '../../../../common/constants'; + +export const createQueryAlertType = (ruleDataClient: RuleDataClient, logger: Logger) => { + const createPersistenceRuleType = createPersistenceRuleTypeFactory({ + ruleDataClient, + logger, + }); + return createPersistenceRuleType({ + id: CUSTOM_ALERT_TYPE_ID, + name: 'Custom Query Rule', + validate: { + params: schema.object({ + indexPatterns: schema.arrayOf(schema.string()), + customQuery: schema.string(), + }), + }, + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + actionVariables: { + context: [{ name: 'server', description: 'the server' }], + }, + minimumLicenseRequired: 'basic', + producer: 'security-solution', + async executor({ + services: { alertWithPersistence, findAlerts }, + params: { indexPatterns, customQuery }, + }) { + try { + const indexPattern: IIndexPattern = { + fields: [], + title: indexPatterns.join(), + }; + + // TODO: kql or lucene? + + const esQuery = buildEsQuery( + indexPattern, + { query: customQuery, language: 'kuery' }, + [] + ) as QueryContainer; + const query: ESSearchRequest = { + body: { + query: esQuery, + fields: ['*'], + sort: { + '@timestamp': 'asc' as const, + }, + }, + }; + + const alerts = await findAlerts(query); + // console.log('alerts', alerts); + alertWithPersistence(alerts).forEach((alert) => { + alert.scheduleActions('default', { server: 'server-test' }); + }); + + return { + lastChecked: new Date(), + }; + } catch (error) { + logger.error(error); + } + }, + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_eql.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_eql.sh new file mode 100755 index 00000000000000..25e247a08ef46d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_eql.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# +# 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. +# + +curl -X POST http://localhost:5601/${BASE_PATH}/api/alerts/alert \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -H 'kbn-xsrf: true' \ + -H 'Content-Type: application/json' \ + --verbose \ + -d ' +{ + "params":{ + "indexPatterns": ["*"], + "eqlQuery": "sequence by host.name↵[any where true]↵[any where true]↵[any where true]" + }, + "consumer":"alerts", + "alertTypeId":"siem.eqlRule", + "schedule":{ + "interval":"1m" + }, + "actions":[], + "tags":[ + "eql", + "persistence" + ], + "notifyWhen":"onActionGroupChange", + "name":"Basic EQL rule" +}' + + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_query.sh new file mode 100755 index 00000000000000..c34af7dee4044d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_query.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# +# 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. +# + +curl -X POST http://localhost:5601/${BASE_PATH}/api/alerts/alert \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -H 'kbn-xsrf: true' \ + -H 'Content-Type: application/json' \ + --verbose \ + -d ' +{ + "params":{ + "indexPatterns": ["*"], + "customQuery": "*:*" + }, + "consumer":"alerts", + "alertTypeId":"siem.customRule", + "schedule":{ + "interval":"1m" + }, + "actions":[], + "tags":[ + "custom", + "persistence" + ], + "notifyWhen":"onActionGroupChange", + "name":"Basic custom query rule" +}' + + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_threshold.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_threshold.sh new file mode 100755 index 00000000000000..8b486b165c34b3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/scripts/create_reference_rule_threshold.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# +# 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. +# + +curl -X POST http://localhost:5601/${BASE_PATH}/api/alerts/alert \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -H 'kbn-xsrf: true' \ + -H 'Content-Type: application/json' \ + --verbose \ + -d ' +{ + "params":{ + "indexPatterns": ["*"], + "customQuery": "*:*", + "thresholdFields": ["source.ip", "destination.ip"], + "thresholdValue": 50, + "thresholdCardinality": [] + }, + "consumer":"alerts", + "alertTypeId":"siem.thresholdRule", + "schedule":{ + "interval":"1m" + }, + "actions":[], + "tags":[ + "persistence", + "threshold" + ], + "notifyWhen":"onActionGroupChange", + "name":"Basic Threshold rule" +}' + + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/threshold.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/threshold.test.ts new file mode 100644 index 00000000000000..36e53b8154e70f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/threshold.test.ts @@ -0,0 +1,132 @@ +/* + * 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. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + +import { createRuleTypeMocks } from './__mocks__/rule_type'; +import { mockThresholdResults } from './__mocks__/threshold'; +import { createThresholdAlertType } from './threshold'; + +describe('Threshold alerts', () => { + it('does not send an alert when threshold is not met', async () => { + const { services, dependencies, executor } = createRuleTypeMocks(); + const thresholdAlertType = createThresholdAlertType( + dependencies.ruleDataClient, + dependencies.logger + ); + + dependencies.alerting.registerType(thresholdAlertType); + + const params = { + indexPatterns: ['*'], + customQuery: '*:*', + thresholdFields: ['source.ip', 'host.name'], + thresholdValue: 4, + }; + + services.scopedClusterClient.asCurrentUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [], + sequences: [], + events: [], + total: { + relation: 'eq', + value: 0, + }, + }, + aggregations: { + 'threshold_0:source.ip': { + buckets: [], + }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + }) + ); + + await executor({ params }); + expect(services.alertInstanceFactory).not.toBeCalled(); + }); + + it('sends a properly formatted alert when threshold is met', async () => { + const { services, dependencies, executor } = createRuleTypeMocks(); + const thresholdAlertType = createThresholdAlertType( + dependencies.ruleDataClient, + dependencies.logger + ); + + dependencies.alerting.registerType(thresholdAlertType); + + const params = { + indexPatterns: ['*'], + customQuery: '*:*', + thresholdFields: ['source.ip', 'host.name'], + thresholdValue: 4, + }; + + services.scopedClusterClient.asCurrentUser.search + .mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [], + total: { + relation: 'eq', + value: 0, + }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + }) + ) + .mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [], + total: { + relation: 'eq', + value: 0, + }, + }, + aggregations: mockThresholdResults.rawResponse.body.aggregations, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + }) + ); + + await executor({ params }); + expect(services.alertInstanceFactory).toBeCalled(); + /* + expect(services.alertWithPersistence).toBeCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + 'event.kind': 'signal', + }), + ]) + ); + */ + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/threshold.ts new file mode 100644 index 00000000000000..d4721e8bab11dc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/threshold.ts @@ -0,0 +1,206 @@ +/* + * 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 moment from 'moment'; +import v4 from 'uuid/v4'; + +import { schema } from '@kbn/config-schema'; +import { Logger } from '@kbn/logging'; + +import { AlertServices } from '../../../../../alerting/server'; +import { + RuleDataClient, + createPersistenceRuleTypeFactory, +} from '../../../../../rule_registry/server'; +import { THRESHOLD_ALERT_TYPE_ID } from '../../../../common/constants'; +import { SignalSearchResponse, ThresholdSignalHistory } from '../signals/types'; +import { + findThresholdSignals, + getThresholdBucketFilters, + getThresholdSignalHistory, + transformThresholdResultsToEcs, +} from '../signals/threshold'; +import { getFilter } from '../signals/get_filter'; +import { BuildRuleMessage } from '../signals/rule_messages'; + +interface RuleParams { + indexPatterns: string[]; + customQuery: string; + thresholdFields: string[]; + thresholdValue: number; + thresholdCardinality: Array<{ + field: string; + value: number; + }>; +} + +interface BulkCreateThresholdSignalParams { + results: SignalSearchResponse; + ruleParams: RuleParams; + services: AlertServices & { logger: Logger }; + inputIndexPattern: string[]; + ruleId: string; + startedAt: Date; + from: Date; + thresholdSignalHistory: ThresholdSignalHistory; + buildRuleMessage: BuildRuleMessage; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const formatThresholdSignals = (params: BulkCreateThresholdSignalParams): any[] => { + const thresholdResults = params.results; + const threshold = { + field: params.ruleParams.thresholdFields, + value: params.ruleParams.thresholdValue, + }; + const results = transformThresholdResultsToEcs( + thresholdResults, + params.ruleParams.indexPatterns.join(','), + params.startedAt, + params.from, + undefined, + params.services.logger, + threshold, + params.ruleId, + undefined, + params.thresholdSignalHistory + ); + return results.hits.hits.map((hit) => { + return { + ...hit, + 'event.kind': 'signal', + 'kibana.rac.alert.id': '???', + 'kibana.rac.alert.uuid': v4(), + '@timestamp': new Date().toISOString(), + }; + }); +}; + +export const createThresholdAlertType = (ruleDataClient: RuleDataClient, logger: Logger) => { + const createPersistenceRuleType = createPersistenceRuleTypeFactory({ + ruleDataClient, + logger, + }); + return createPersistenceRuleType({ + id: THRESHOLD_ALERT_TYPE_ID, + name: 'Threshold Rule', + validate: { + params: schema.object({ + indexPatterns: schema.arrayOf(schema.string()), + customQuery: schema.string(), + thresholdFields: schema.arrayOf(schema.string()), + thresholdValue: schema.number(), + thresholdCardinality: schema.arrayOf( + schema.object({ + field: schema.string(), + value: schema.number(), + }) + ), + }), + }, + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + actionVariables: { + context: [{ name: 'server', description: 'the server' }], + }, + minimumLicenseRequired: 'basic', + producer: 'security-solution', + async executor({ startedAt, services, params, alertId }) { + const fromDate = moment(startedAt).subtract(moment.duration(5, 'm')); // hardcoded 5-minute rule interval + const from = fromDate.toISOString(); + const to = startedAt.toISOString(); + + // TODO: how to get the output index? + const outputIndex = ['.kibana-madi-8-alerts-security-solution-8.0.0-000001']; + const buildRuleMessage = (...messages: string[]) => messages.join(); + const timestampOverride = undefined; + + const { + thresholdSignalHistory, + searchErrors: previousSearchErrors, + } = await getThresholdSignalHistory({ + indexPattern: outputIndex, + from, + to, + services: (services as unknown) as AlertServices, + logger, + ruleId: alertId, + bucketByFields: params.thresholdFields, + timestampOverride, + buildRuleMessage, + }); + + const bucketFilters = await getThresholdBucketFilters({ + thresholdSignalHistory, + timestampOverride, + }); + + const esFilter = await getFilter({ + type: 'threshold', + filters: bucketFilters, + language: 'kuery', + query: params.customQuery, + savedId: undefined, + services: (services as unknown) as AlertServices, + index: params.indexPatterns, + lists: [], + }); + + const { + searchResult: thresholdResults, + searchErrors, + searchDuration: thresholdSearchDuration, + } = await findThresholdSignals({ + inputIndexPattern: params.indexPatterns, + from, + to, + services: (services as unknown) as AlertServices, + logger, + filter: esFilter, + threshold: { + field: params.thresholdFields, + value: params.thresholdValue, + cardinality: params.thresholdCardinality, + }, + timestampOverride, + buildRuleMessage, + }); + + logger.info(`Threshold search took ${thresholdSearchDuration}ms`); // TODO: rule status service + + const alerts = formatThresholdSignals({ + results: thresholdResults, + ruleParams: params, + services: (services as unknown) as AlertServices & { logger: Logger }, + inputIndexPattern: ['TODO'], + ruleId: alertId, + startedAt, + from: fromDate.toDate(), + thresholdSignalHistory, + buildRuleMessage, + }); + + const errors = searchErrors.concat(previousSearchErrors); + if (errors.length === 0) { + services.alertWithPersistence(alerts).forEach((alert) => { + alert.scheduleActions('default', { server: 'server-test' }); + }); + } else { + throw new Error(errors.join('\n')); + } + + return { + lastChecked: new Date(), + }; + }, + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts index 6af4397a4193a6..3527e43c03d52b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts @@ -6,15 +6,17 @@ */ import { transformError, getIndexExists } from '@kbn/securitysolution-es-utils'; +import { parseExperimentalConfigValue } from '../../../../../common/experimental_features'; +import { ConfigType } from '../../../../config'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; +import { DEFAULT_ALERTS_INDEX, DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; import { buildSiemResponse } from '../utils'; import { SIGNALS_TEMPLATE_VERSION } from './get_signals_template'; import { getIndexVersion } from './get_index_version'; import { isOutdated } from '../../migrations/helpers'; -export const readIndexRoute = (router: SecuritySolutionPluginRouter) => { +export const readIndexRoute = (router: SecuritySolutionPluginRouter, config: ConfigType) => { router.get( { path: DETECTION_ENGINE_INDEX_URL, @@ -34,8 +36,16 @@ export const readIndexRoute = (router: SecuritySolutionPluginRouter) => { return siemResponse.error({ statusCode: 404 }); } + // TODO: Once we are past experimental phase this code should be removed + const { ruleRegistryEnabled } = parseExperimentalConfigValue(config.enableExperimental); + if (ruleRegistryEnabled) { + return response.ok({ + body: { name: DEFAULT_ALERTS_INDEX, index_mapping_outdated: false }, + }); + } + const index = siemClient.getSignalsIndex(); - const indexExists = await getIndexExists(esClient, index); + const indexExists = ruleRegistryEnabled ? true : await getIndexExists(esClient, index); if (indexExists) { let mappingOutdated: boolean | null = null; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index 9b7e7bb42f4236..993d9300e414f4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -6,6 +6,7 @@ */ import { transformError, getIndexExists } from '@kbn/securitysolution-es-utils'; +import { RuleDataClient } from '../../../../../../rule_registry/server'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; @@ -24,7 +25,8 @@ import { convertCreateAPIToInternalSchema } from '../../schemas/rule_converters' export const createRulesRoute = ( router: SecuritySolutionPluginRouter, - ml: SetupPlugins['ml'] + ml: SetupPlugins['ml'], + ruleDataClient?: RuleDataClient | null ): void => { router.post( { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts index 76fb9ac0c77e33..4b05f603b85b7c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -6,6 +6,7 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { RuleDataClient } from '../../../../../../rule_registry/server'; import { queryRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/query_rules_type_dependents'; import { queryRulesSchema, @@ -22,7 +23,10 @@ import { deleteNotifications } from '../../notifications/delete_notifications'; import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; -export const deleteRulesRoute = (router: SecuritySolutionPluginRouter) => { +export const deleteRulesRoute = ( + router: SecuritySolutionPluginRouter, + ruleDataClient?: RuleDataClient | null +) => { router.delete( { path: DETECTION_ENGINE_RULES_URL, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts index 347d005c58a4a8..428978fe1d8207 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -6,6 +6,7 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { RuleDataClient } from '../../../../../../rule_registry/server'; import { findRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/find_rules_type_dependents'; import { findRulesSchema, @@ -20,7 +21,10 @@ import { buildRouteValidation } from '../../../../utils/build_validation/route_v import { transformFindAlerts } from './utils'; import { getBulkRuleActionsSavedObject } from '../../rule_actions/get_bulk_rule_actions_saved_object'; -export const findRulesRoute = (router: SecuritySolutionPluginRouter) => { +export const findRulesRoute = ( + router: SecuritySolutionPluginRouter, + ruleDataClient?: RuleDataClient | null +) => { router.get( { path: `${DETECTION_ENGINE_RULES_URL}/_find`, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts index 780c248183ab96..eaaa44fcf19160 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -6,6 +6,7 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { RuleDataClient } from '../../../../../../rule_registry/server'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { patchRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/patch_rules_type_dependents'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; @@ -28,7 +29,11 @@ import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_s import { readRules } from '../../rules/read_rules'; import { PartialFilter } from '../../types'; -export const patchRulesRoute = (router: SecuritySolutionPluginRouter, ml: SetupPlugins['ml']) => { +export const patchRulesRoute = ( + router: SecuritySolutionPluginRouter, + ml: SetupPlugins['ml'], + ruleDataClient?: RuleDataClient | null +) => { router.patch( { path: DETECTION_ENGINE_RULES_URL, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts index ac45e5d2ed3b20..917da6c9708d54 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -6,6 +6,7 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { RuleDataClient } from '../../../../../../rule_registry/server'; import { queryRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/query_rules_type_dependents'; import { queryRulesSchema, @@ -21,7 +22,10 @@ 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'; -export const readRulesRoute = (router: SecuritySolutionPluginRouter) => { +export const readRulesRoute = ( + router: SecuritySolutionPluginRouter, + ruleDataClient?: RuleDataClient | null +) => { router.get( { path: DETECTION_ENGINE_RULES_URL, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts index aad0068758f7dc..0ff6cb3cd2d0f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -6,6 +6,7 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { RuleDataClient } from '../../../../../../rule_registry/server'; import { updateRulesSchema } from '../../../../../common/detection_engine/schemas/request'; import { updateRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/update_rules_type_dependents'; import type { SecuritySolutionPluginRouter } from '../../../../types'; @@ -22,7 +23,11 @@ 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 = (router: SecuritySolutionPluginRouter, ml: SetupPlugins['ml']) => { +export const updateRulesRoute = ( + router: SecuritySolutionPluginRouter, + ml: SetupPlugins['ml'], + ruleDataClient?: RuleDataClient | null +) => { router.put( { path: DETECTION_ENGINE_RULES_URL, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.test.ts index 909c94f145528c..d6b998e3142349 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.test.ts @@ -14,7 +14,7 @@ import { getSignalsAggsAndQueryRequest, getEmptySignalsResponse, } from '../__mocks__/request_responses'; -import { requestContextMock, serverMock, requestMock } from '../__mocks__'; +import { requestContextMock, serverMock, requestMock, createMockConfig } from '../__mocks__'; import { querySignalsRoute } from './query_signals_route'; describe('query for signal', () => { @@ -27,7 +27,7 @@ describe('query for signal', () => { clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptySignalsResponse()); - querySignalsRoute(server.router); + querySignalsRoute(server.router, createMockConfig()); }); describe('query and agg on signals index', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts index 91172a277bf54a..770c1a5da344f6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -6,8 +6,13 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { parseExperimentalConfigValue } from '../../../../../common/experimental_features'; +import { ConfigType } from '../../../../config'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; +import { + DEFAULT_ALERTS_INDEX, + DETECTION_ENGINE_QUERY_SIGNALS_URL, +} from '../../../../../common/constants'; import { buildSiemResponse } from '../utils'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; @@ -16,7 +21,7 @@ import { QuerySignalsSchemaDecoded, } from '../../../../../common/detection_engine/schemas/request/query_signals_index_schema'; -export const querySignalsRoute = (router: SecuritySolutionPluginRouter) => { +export const querySignalsRoute = (router: SecuritySolutionPluginRouter, config: ConfigType) => { router.post( { path: DETECTION_ENGINE_QUERY_SIGNALS_URL, @@ -48,9 +53,12 @@ export const querySignalsRoute = (router: SecuritySolutionPluginRouter) => { const clusterClient = context.core.elasticsearch.legacy.client; const siemClient = context.securitySolution!.getAppClient(); + // TODO: Once we are past experimental phase this code should be removed + const { ruleRegistryEnabled } = parseExperimentalConfigValue(config.enableExperimental); + try { const result = await clusterClient.callAsCurrentUser('search', { - index: siemClient.getSignalsIndex(), + index: ruleRegistryEnabled ? DEFAULT_ALERTS_INDEX : siemClient.getSignalsIndex(), body: { query, aggs, _source, track_total_hits, size }, ignoreUnavailable: true, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts index 986393d6d34546..ca7f22e4a75707 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts @@ -138,7 +138,7 @@ export const findThresholdSignals = async ({ logger, // @ts-expect-error refactor to pass type explicitly instead of unknown filter, - pageSize: 1, + pageSize: 0, sortOrder: 'desc', buildRuleMessage, }); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index efeabc844a810e..a71e957657909b 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { once } from 'lodash'; import { Observable } from 'rxjs'; import { i18n } from '@kbn/i18n'; import LRU from 'lru-cache'; @@ -27,7 +28,17 @@ import { PluginSetupContract as AlertingSetup, PluginStartContract as AlertPluginStartContract, } from '../../alerting/server'; + +import { + ECS_COMPONENT_TEMPLATE_NAME, + TECHNICAL_COMPONENT_TEMPLATE_NAME, +} from '../../rule_registry/common/assets'; import { SecurityPluginSetup as SecuritySetup, SecurityPluginStart } from '../../security/server'; +import { + RuleDataClient, + RuleRegistryPluginSetupContract, + RuleRegistryPluginStartContract, +} from '../../rule_registry/server'; import { PluginSetupContract as FeaturesSetup } from '../../features/server'; import { MlPluginSetup as MlSetup } from '../../ml/server'; import { ListPluginSetup } from '../../lists/server'; @@ -37,6 +48,9 @@ import { ILicense, LicensingPluginStart } from '../../licensing/server'; import { FleetStartContract } from '../../fleet/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { compose } from './lib/compose/kibana'; +import { createQueryAlertType } from './lib/detection_engine/reference_rules/query'; +import { createEqlAlertType } from './lib/detection_engine/reference_rules/eql'; +import { createThresholdAlertType } from './lib/detection_engine/reference_rules/threshold'; import { initRoutes } from './routes'; import { isAlertExecutor } from './lib/detection_engine/signals/types'; import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type'; @@ -53,6 +67,8 @@ import { SecurityPageName, SIGNALS_ID, NOTIFICATIONS_ID, + REFERENCE_RULE_ALERT_TYPE_ID, + REFERENCE_RULE_PERSISTENCE_ALERT_TYPE_ID, } from '../common/constants'; import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerLimitedConcurrencyRoutes } from './endpoint/routes/limited_concurrency'; @@ -86,6 +102,7 @@ export interface SetupPlugins { features: FeaturesSetup; lists?: ListPluginSetup; ml?: MlSetup; + ruleRegistry: RuleRegistryPluginSetupContract; security?: SecuritySetup; spaces?: SpacesSetup; taskManager?: TaskManagerSetupContract; @@ -98,6 +115,7 @@ export interface StartPlugins { data: DataPluginStart; fleet?: FleetStartContract; licensing: LicensingPluginStart; + ruleRegistry: RuleRegistryPluginStartContract; taskManager?: TaskManagerStartContract; telemetry?: TelemetryPluginStart; security: SecurityPluginStart; @@ -133,6 +151,7 @@ export class Plugin implements IPlugin, plugins: SetupPlugins) { this.logger.debug('plugin setup'); + this.setupPlugins = plugins; const config = this.config; const globalConfig = this.context.config.legacy.get(); @@ -193,13 +213,75 @@ export class Plugin implements IPlugin core.getStartServices().then(([coreStart]) => coreStart); + + const ready = once(async () => { + const componentTemplateName = ruleDataService.getFullAssetName( + 'security-solution-mappings' + ); + + if (!ruleDataService.isWriteEnabled()) { + return; + } + + await ruleDataService.createOrUpdateComponentTemplate({ + name: componentTemplateName, + body: { + template: { + settings: { + number_of_shards: 1, + }, + mappings: {}, // TODO: Add mappings here via `mappingFromFieldMap()` + }, + }, + }); + + await ruleDataService.createOrUpdateIndexTemplate({ + name: ruleDataService.getFullAssetName('security-solution-index-template'), + body: { + index_patterns: [ruleDataService.getFullAssetName('security-solution*')], + composed_of: [ + ruleDataService.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME), + ruleDataService.getFullAssetName(ECS_COMPONENT_TEMPLATE_NAME), + componentTemplateName, + ], + }, + }); + }); + + ready().catch((err) => { + this.logger!.error(err); + }); + + ruleDataClient = new RuleDataClient({ + alias: plugins.ruleRegistry.ruleDataService.getFullAssetName('security-solution'), + getClusterClient: async () => { + const coreStart = await start(); + return coreStart.elasticsearch.client.asInternalUser; + }, + ready, + }); + + // Register reference rule types via rule-registry + this.setupPlugins.alerting.registerType(createQueryAlertType(ruleDataClient, this.logger)); + this.setupPlugins.alerting.registerType(createEqlAlertType(ruleDataClient, this.logger)); + this.setupPlugins.alerting.registerType( + createThresholdAlertType(ruleDataClient, this.logger) + ); + } + // TO DO We need to get the endpoint routes inside of initRoutes initRoutes( router, config, plugins.encryptedSavedObjects?.canEncrypt === true, plugins.security, - plugins.ml + plugins.ml, + ruleDataClient ); registerEndpointRoutes(router, endpointContext); registerLimitedConcurrencyRoutes(core); @@ -208,6 +290,16 @@ export class Plugin implements IPlugin { // Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules // All REST rule creation, deletion, updating, etc...... - createRulesRoute(router, ml); - readRulesRoute(router); - updateRulesRoute(router, ml); - patchRulesRoute(router, ml); - deleteRulesRoute(router); - findRulesRoute(router); + createRulesRoute(router, ml, ruleDataClient); + readRulesRoute(router, ruleDataClient); + updateRulesRoute(router, ml, ruleDataClient); + patchRulesRoute(router, ml, ruleDataClient); + deleteRulesRoute(router, ruleDataClient); + findRulesRoute(router, ruleDataClient); + + // TODO: pass ruleDataClient to all relevant routes addPrepackedRulesRoute(router, config, security); getPrepackagedRulesStatusRoute(router, config, security); @@ -102,7 +107,7 @@ export const initRoutes = ( // POST /api/detection_engine/signals/status // Example usage can be found in security_solution/server/lib/detection_engine/scripts/signals setSignalsStatusRoute(router); - querySignalsRoute(router); + querySignalsRoute(router, config); getSignalsMigrationStatusRoute(router); createSignalsMigrationRoute(router, security); finalizeSignalsMigrationRoute(router, security); @@ -111,7 +116,7 @@ export const initRoutes = ( // Detection Engine index routes that have the REST endpoints of /api/detection_engine/index // All REST index creation, policy management for spaces createIndexRoute(router); - readIndexRoute(router); + readIndexRoute(router, config); deleteIndexRoute(router); // Detection Engine tags routes that have the REST endpoints of /api/detection_engine/tags diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx index e43db6b86f8b90..f489fd0c16455b 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx @@ -33,6 +33,7 @@ const mockDeps = { trustedAppsByPolicyEnabled: false, metricsEntitiesEnabled: false, hostIsolationEnabled: false, + ruleRegistryEnabled: false, }, service: {} as EndpointAppContextService, } as EndpointAppContext,