From 5d1ef0e7e58868b32eeb6130b6a94a65cc5ec912 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Fri, 25 Feb 2022 14:41:58 -0500 Subject: [PATCH 1/3] [Response Ops] Alert search strategy (#124430) * Initial code for search strategy in rule registry for use in triggers actions ui * WIP * More * Bump this up * Add a couple basic tests * More separation * Some api tests * Fix types * fix type * Remove tests * add this back in, not sure why this happened * Remove test code * PR feedback * Fix typing * Fix unit tests * Skip this test due to errors * Add more tests * Use fields api * Add issue link * PR feedback * Fix types and test * Use nested key TS definition Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/rule_registry/common/constants.ts | 1 + x-pack/plugins/rule_registry/common/index.ts | 4 +- .../common/search_strategy/index.ts | 58 +++++ x-pack/plugins/rule_registry/kibana.json | 2 +- .../server/alert_data_client/alerts_client.ts | 18 +- .../server/lib/get_authz_filter.test.ts | 20 ++ .../server/lib/get_authz_filter.ts | 33 +++ .../server/lib/get_spaces_filter.test.ts | 20 ++ .../server/lib/get_spaces_filter.ts | 11 + .../plugins/rule_registry/server/lib/index.ts | 8 + x-pack/plugins/rule_registry/server/plugin.ts | 25 +++ .../server/search_strategy/index.ts | 8 + .../search_strategy/search_strategy.test.ts | 202 ++++++++++++++++++ .../server/search_strategy/search_strategy.ts | 149 +++++++++++++ x-pack/plugins/rule_registry/tsconfig.json | 1 - .../plugins/triggers_actions_ui/tsconfig.json | 1 + .../security_and_spaces/tests/basic/index.ts | 15 +- .../tests/basic/search_strategy.ts | 138 ++++++++++++ 18 files changed, 691 insertions(+), 23 deletions(-) create mode 100644 x-pack/plugins/rule_registry/common/search_strategy/index.ts create mode 100644 x-pack/plugins/rule_registry/server/lib/get_authz_filter.test.ts create mode 100644 x-pack/plugins/rule_registry/server/lib/get_authz_filter.ts create mode 100644 x-pack/plugins/rule_registry/server/lib/get_spaces_filter.test.ts create mode 100644 x-pack/plugins/rule_registry/server/lib/get_spaces_filter.ts create mode 100644 x-pack/plugins/rule_registry/server/lib/index.ts create mode 100644 x-pack/plugins/rule_registry/server/search_strategy/index.ts create mode 100644 x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts create mode 100644 x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts create mode 100644 x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts diff --git a/x-pack/plugins/rule_registry/common/constants.ts b/x-pack/plugins/rule_registry/common/constants.ts index 72793b1087e7b..1c5fad0e2215f 100644 --- a/x-pack/plugins/rule_registry/common/constants.ts +++ b/x-pack/plugins/rule_registry/common/constants.ts @@ -6,3 +6,4 @@ */ export const BASE_RAC_ALERTS_API_PATH = '/internal/rac/alerts'; +export const MAX_ALERT_SEARCH_SIZE = 1000; diff --git a/x-pack/plugins/rule_registry/common/index.ts b/x-pack/plugins/rule_registry/common/index.ts index 5d36cd8cad7be..2dd7f6bbc456e 100644 --- a/x-pack/plugins/rule_registry/common/index.ts +++ b/x-pack/plugins/rule_registry/common/index.ts @@ -4,4 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -export { parseTechnicalFields } from './parse_technical_fields'; +export { parseTechnicalFields, type ParsedTechnicalFields } from './parse_technical_fields'; +export type { RuleRegistrySearchRequest, RuleRegistrySearchResponse } from './search_strategy'; +export { BASE_RAC_ALERTS_API_PATH } from './constants'; diff --git a/x-pack/plugins/rule_registry/common/search_strategy/index.ts b/x-pack/plugins/rule_registry/common/search_strategy/index.ts new file mode 100644 index 0000000000000..efb8a3478263e --- /dev/null +++ b/x-pack/plugins/rule_registry/common/search_strategy/index.ts @@ -0,0 +1,58 @@ +/* + * 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 { ValidFeatureId } from '@kbn/rule-data-utils'; +import { Ecs } from 'kibana/server'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { IEsSearchRequest, IEsSearchResponse } from 'src/plugins/data/common'; + +export type RuleRegistrySearchRequest = IEsSearchRequest & { + featureIds: ValidFeatureId[]; + query?: { bool: estypes.QueryDslBoolQuery }; +}; + +type Prev = [ + never, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + ...Array<0> +]; + +type Join = K extends string | number + ? P extends string | number + ? `${K}${'' extends P ? '' : '.'}${P}` + : never + : never; + +type DotNestedKeys = [D] extends [never] + ? never + : T extends object + ? { [K in keyof T]-?: Join> }[keyof T] + : ''; + +type EcsFieldsResponse = { + [Property in DotNestedKeys]: string[]; +}; +export type RuleRegistrySearchResponse = IEsSearchResponse; diff --git a/x-pack/plugins/rule_registry/kibana.json b/x-pack/plugins/rule_registry/kibana.json index 75e0c2c8c0bac..9603cb0a2640b 100644 --- a/x-pack/plugins/rule_registry/kibana.json +++ b/x-pack/plugins/rule_registry/kibana.json @@ -8,6 +8,6 @@ "kibanaVersion": "kibana", "configPath": ["xpack", "ruleRegistry"], "requiredPlugins": ["alerting", "data", "triggersActionsUi"], - "optionalPlugins": ["security"], + "optionalPlugins": ["security", "spaces"], "server": true } diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts index 1f8cfb4b78c85..a97b43332e0a9 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -21,7 +21,7 @@ import { InlineScript, QueryDslQueryContainer, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { AlertTypeParams, AlertingAuthorizationFilterType } from '../../../alerting/server'; +import { AlertTypeParams } from '../../../alerting/server'; import { ReadOperations, AlertingAuthorization, @@ -39,6 +39,7 @@ import { } from '../../common/technical_rule_data_field_names'; import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; import { Dataset, IRuleDataService } from '../rule_data_plugin_service'; +import { getAuthzFilter, getSpacesFilter } from '../lib'; // TODO: Fix typings https://github.com/elastic/kibana/issues/101776 type NonNullableProps = Omit & { @@ -369,14 +370,8 @@ export class AlertsClient { config: EsQueryConfig ) { try { - const { filter: authzFilter } = await this.authorization.getAuthorizationFilter( - AlertingAuthorizationEntity.Alert, - { - type: AlertingAuthorizationFilterType.ESDSL, - fieldNames: { consumer: ALERT_RULE_CONSUMER, ruleTypeId: ALERT_RULE_TYPE_ID }, - }, - operation - ); + const authzFilter = (await getAuthzFilter(this.authorization, operation)) as Filter; + const spacesFilter = getSpacesFilter(alertSpaceId) as unknown as Filter; let esQuery; if (id != null) { esQuery = { query: `_id:${id}`, language: 'kuery' }; @@ -388,10 +383,7 @@ export class AlertsClient { const builtQuery = buildEsQuery( undefined, esQuery == null ? { query: ``, language: 'kuery' } : esQuery, - [ - authzFilter as unknown as Filter, - { query: { term: { [SPACE_IDS]: alertSpaceId } } } as unknown as Filter, - ], + [authzFilter, spacesFilter], config ); if (query != null && typeof query === 'object') { diff --git a/x-pack/plugins/rule_registry/server/lib/get_authz_filter.test.ts b/x-pack/plugins/rule_registry/server/lib/get_authz_filter.test.ts new file mode 100644 index 0000000000000..3b79c7a5bad8a --- /dev/null +++ b/x-pack/plugins/rule_registry/server/lib/get_authz_filter.test.ts @@ -0,0 +1,20 @@ +/* + * 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 { alertingAuthorizationMock } from '../../../alerting/server/authorization/alerting_authorization.mock'; +import { ReadOperations } from '../../../alerting/server'; +import { getAuthzFilter } from './get_authz_filter'; + +describe('getAuthzFilter()', () => { + it('should call `getAuthorizationFilter`', async () => { + const authorization = alertingAuthorizationMock.create(); + authorization.getAuthorizationFilter.mockImplementationOnce(async () => { + return { filter: { test: true }, ensureRuleTypeIsAuthorized: () => {} }; + }); + const filter = await getAuthzFilter(authorization, ReadOperations.Find); + expect(filter).toStrictEqual({ test: true }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/lib/get_authz_filter.ts b/x-pack/plugins/rule_registry/server/lib/get_authz_filter.ts new file mode 100644 index 0000000000000..88b8feb2ca97c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/lib/get_authz_filter.ts @@ -0,0 +1,33 @@ +/* + * 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 { PublicMethodsOf } from '@kbn/utility-types'; +import { + ReadOperations, + WriteOperations, + AlertingAuthorization, + AlertingAuthorizationEntity, + AlertingAuthorizationFilterType, +} from '../../../alerting/server'; +import { + ALERT_RULE_CONSUMER, + ALERT_RULE_TYPE_ID, +} from '../../common/technical_rule_data_field_names'; + +export async function getAuthzFilter( + authorization: PublicMethodsOf, + operation: WriteOperations.Update | ReadOperations.Get | ReadOperations.Find +) { + const { filter } = await authorization.getAuthorizationFilter( + AlertingAuthorizationEntity.Alert, + { + type: AlertingAuthorizationFilterType.ESDSL, + fieldNames: { consumer: ALERT_RULE_CONSUMER, ruleTypeId: ALERT_RULE_TYPE_ID }, + }, + operation + ); + return filter; +} diff --git a/x-pack/plugins/rule_registry/server/lib/get_spaces_filter.test.ts b/x-pack/plugins/rule_registry/server/lib/get_spaces_filter.test.ts new file mode 100644 index 0000000000000..7fd5f00fd99b1 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/lib/get_spaces_filter.test.ts @@ -0,0 +1,20 @@ +/* + * 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 { getSpacesFilter } from '.'; +describe('getSpacesFilter()', () => { + it('should return a spaces filter', () => { + expect(getSpacesFilter('1')).toStrictEqual({ + term: { + 'kibana.space_ids': '1', + }, + }); + }); + + it('should return undefined if no space id is provided', () => { + expect(getSpacesFilter()).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/lib/get_spaces_filter.ts b/x-pack/plugins/rule_registry/server/lib/get_spaces_filter.ts new file mode 100644 index 0000000000000..2756b3d600f18 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/lib/get_spaces_filter.ts @@ -0,0 +1,11 @@ +/* + * 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 { SPACE_IDS } from '../../common/technical_rule_data_field_names'; + +export function getSpacesFilter(spaceId?: string) { + return spaceId ? { term: { [SPACE_IDS]: spaceId } } : undefined; +} diff --git a/x-pack/plugins/rule_registry/server/lib/index.ts b/x-pack/plugins/rule_registry/server/lib/index.ts new file mode 100644 index 0000000000000..c9ed157e7c18a --- /dev/null +++ b/x-pack/plugins/rule_registry/server/lib/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { getAuthzFilter } from './get_authz_filter'; +export { getSpacesFilter } from './get_spaces_filter'; diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index 713e7862207b8..292e987879d58 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -17,6 +17,11 @@ import { import { PluginStartContract as AlertingStart } from '../../alerting/server'; import { SecurityPluginSetup } from '../../security/server'; +import { SpacesPluginStart } from '../../spaces/server'; +import { + PluginStart as DataPluginStart, + PluginSetup as DataPluginSetup, +} from '../../../../src/plugins/data/server'; import { RuleRegistryPluginConfig } from './config'; import { IRuleDataService, RuleDataService } from './rule_data_plugin_service'; @@ -24,13 +29,17 @@ import { AlertsClientFactory } from './alert_data_client/alerts_client_factory'; import { AlertsClient } from './alert_data_client/alerts_client'; import { RacApiRequestHandlerContext, RacRequestHandlerContext } from './types'; import { defineRoutes } from './routes'; +import { ruleRegistrySearchStrategyProvider } from './search_strategy'; export interface RuleRegistryPluginSetupDependencies { security?: SecurityPluginSetup; + data: DataPluginSetup; } export interface RuleRegistryPluginStartDependencies { alerting: AlertingStart; + data: DataPluginStart; + spaces?: SpacesPluginStart; } export interface RuleRegistryPluginSetupContract { @@ -95,6 +104,22 @@ export class RuleRegistryPlugin this.ruleDataService.initializeService(); + core.getStartServices().then(([_, depsStart]) => { + const ruleRegistrySearchStrategy = ruleRegistrySearchStrategyProvider( + depsStart.data, + this.ruleDataService!, + depsStart.alerting, + logger, + plugins.security, + depsStart.spaces + ); + + plugins.data.search.registerSearchStrategy( + 'ruleRegistryAlertsSearchStrategy', + ruleRegistrySearchStrategy + ); + }); + // ALERTS ROUTES const router = core.http.createRouter(); core.http.registerRouteHandlerContext( diff --git a/x-pack/plugins/rule_registry/server/search_strategy/index.ts b/x-pack/plugins/rule_registry/server/search_strategy/index.ts new file mode 100644 index 0000000000000..63f39430a5522 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/search_strategy/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ruleRegistrySearchStrategyProvider } from './search_strategy'; diff --git a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts new file mode 100644 index 0000000000000..9f83930dadc69 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts @@ -0,0 +1,202 @@ +/* + * 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 { merge } from 'lodash'; +import { loggerMock } from '@kbn/logging-mocks'; +import { AlertConsumers } from '@kbn/rule-data-utils'; +import { ruleRegistrySearchStrategyProvider, EMPTY_RESPONSE } from './search_strategy'; +import { ruleDataServiceMock } from '../rule_data_plugin_service/rule_data_plugin_service.mock'; +import { dataPluginMock } from '../../../../../src/plugins/data/server/mocks'; +import { SearchStrategyDependencies } from '../../../../../src/plugins/data/server'; +import { alertsMock } from '../../../alerting/server/mocks'; +import { securityMock } from '../../../security/server/mocks'; +import { spacesMock } from '../../../spaces/server/mocks'; +import { RuleRegistrySearchRequest } from '../../common/search_strategy'; +import { IndexInfo } from '../rule_data_plugin_service/index_info'; +import * as getAuthzFilterImport from '../lib/get_authz_filter'; + +const getBasicResponse = (overwrites = {}) => { + return merge( + { + isPartial: false, + isRunning: false, + total: 0, + loaded: 0, + rawResponse: { + took: 1, + timed_out: false, + _shards: { + failed: 0, + successful: 1, + total: 1, + }, + hits: { + max_score: 0, + hits: [], + total: 0, + }, + }, + }, + overwrites + ); +}; + +describe('ruleRegistrySearchStrategyProvider()', () => { + const data = dataPluginMock.createStartContract(); + const ruleDataService = ruleDataServiceMock.create(); + const alerting = alertsMock.createStart(); + const security = securityMock.createSetup(); + const spaces = spacesMock.createStart(); + const logger = loggerMock.create(); + + const response = getBasicResponse({ + rawResponse: { + hits: { + hits: [ + { + _source: { + foo: 1, + }, + }, + ], + }, + }, + }); + + let getAuthzFilterSpy: jest.SpyInstance; + + beforeEach(() => { + ruleDataService.findIndicesByFeature.mockImplementation(() => { + return [ + { + baseName: 'test', + } as IndexInfo, + ]; + }); + + data.search.getSearchStrategy.mockImplementation(() => { + return { + search: () => of(response), + }; + }); + + getAuthzFilterSpy = jest + .spyOn(getAuthzFilterImport, 'getAuthzFilter') + .mockImplementation(async () => { + return {}; + }); + }); + + afterEach(() => { + ruleDataService.findIndicesByFeature.mockClear(); + data.search.getSearchStrategy.mockClear(); + getAuthzFilterSpy.mockClear(); + }); + + it('should handle a basic search request', async () => { + const request: RuleRegistrySearchRequest = { + featureIds: [AlertConsumers.LOGS], + }; + const options = {}; + const deps = { + request: {}, + }; + + const strategy = ruleRegistrySearchStrategyProvider( + data, + ruleDataService, + alerting, + logger, + security, + spaces + ); + + const result = await strategy + .search(request, options, deps as unknown as SearchStrategyDependencies) + .toPromise(); + expect(result).toBe(response); + }); + + it('should use the active space in siem queries', async () => { + const request: RuleRegistrySearchRequest = { + featureIds: [AlertConsumers.SIEM], + }; + const options = {}; + const deps = { + request: {}, + }; + + spaces.spacesService.getActiveSpace.mockImplementation(async () => { + return { + id: 'testSpace', + name: 'Test Space', + disabledFeatures: [], + }; + }); + + ruleDataService.findIndicesByFeature.mockImplementation(() => { + return [ + { + baseName: 'myTestIndex', + } as unknown as IndexInfo, + ]; + }); + + let searchRequest: RuleRegistrySearchRequest = {} as unknown as RuleRegistrySearchRequest; + data.search.getSearchStrategy.mockImplementation(() => { + return { + search: (_request) => { + searchRequest = _request as unknown as RuleRegistrySearchRequest; + return of(response); + }, + }; + }); + + const strategy = ruleRegistrySearchStrategyProvider( + data, + ruleDataService, + alerting, + logger, + security, + spaces + ); + + await strategy + .search(request, options, deps as unknown as SearchStrategyDependencies) + .toPromise(); + spaces.spacesService.getActiveSpace.mockClear(); + expect(searchRequest?.params?.index).toStrictEqual(['myTestIndex-testSpace*']); + }); + + it('should return an empty response if no valid indices are found', async () => { + const request: RuleRegistrySearchRequest = { + featureIds: [AlertConsumers.LOGS], + }; + const options = {}; + const deps = { + request: {}, + }; + + ruleDataService.findIndicesByFeature.mockImplementationOnce(() => { + return []; + }); + + const strategy = ruleRegistrySearchStrategyProvider( + data, + ruleDataService, + alerting, + logger, + security, + spaces + ); + + const result = await strategy + .search(request, options, deps as unknown as SearchStrategyDependencies) + .toPromise(); + expect(result).toBe(EMPTY_RESPONSE); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts new file mode 100644 index 0000000000000..dd7f392b0a268 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts @@ -0,0 +1,149 @@ +/* + * 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 { map, mergeMap, catchError } from 'rxjs/operators'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { Logger } from 'src/core/server'; +import { from, of } from 'rxjs'; +import { isValidFeatureId } from '@kbn/rule-data-utils'; +import { ENHANCED_ES_SEARCH_STRATEGY } from '../../../../../src/plugins/data/common'; +import { ISearchStrategy, PluginStart } from '../../../../../src/plugins/data/server'; +import { + RuleRegistrySearchRequest, + RuleRegistrySearchResponse, +} from '../../common/search_strategy'; +import { ReadOperations, PluginStartContract as AlertingStart } from '../../../alerting/server'; +import { SecurityPluginSetup } from '../../../security/server'; +import { SpacesPluginStart } from '../../../spaces/server'; +import { IRuleDataService } from '..'; +import { Dataset } from '../rule_data_plugin_service/index_options'; +import { MAX_ALERT_SEARCH_SIZE } from '../../common/constants'; +import { AlertAuditAction, alertAuditEvent } from '../'; +import { getSpacesFilter, getAuthzFilter } from '../lib'; + +export const EMPTY_RESPONSE: RuleRegistrySearchResponse = { + rawResponse: {} as RuleRegistrySearchResponse['rawResponse'], +}; + +export const ruleRegistrySearchStrategyProvider = ( + data: PluginStart, + ruleDataService: IRuleDataService, + alerting: AlertingStart, + logger: Logger, + security?: SecurityPluginSetup, + spaces?: SpacesPluginStart +): ISearchStrategy => { + const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); + + return { + search: (request, options, deps) => { + const securityAuditLogger = security?.audit.asScoped(deps.request); + const getActiveSpace = async () => spaces?.spacesService.getActiveSpace(deps.request); + const getAsync = async () => { + const [space, authorization] = await Promise.all([ + getActiveSpace(), + alerting.getAlertingAuthorizationWithRequest(deps.request), + ]); + const authzFilter = (await getAuthzFilter( + authorization, + ReadOperations.Find + )) as estypes.QueryDslQueryContainer; + return { space, authzFilter }; + }; + return from(getAsync()).pipe( + mergeMap(({ space, authzFilter }) => { + const indices: string[] = request.featureIds.reduce((accum: string[], featureId) => { + if (!isValidFeatureId(featureId)) { + logger.warn( + `Found invalid feature '${featureId}' while using rule registry search strategy. No alert data from this feature will be searched.` + ); + return accum; + } + + return [ + ...accum, + ...ruleDataService + .findIndicesByFeature(featureId, Dataset.alerts) + .map((indexInfo) => { + return featureId === 'siem' + ? `${indexInfo.baseName}-${space?.id ?? ''}*` + : `${indexInfo.baseName}*`; + }), + ]; + }, []); + + if (indices.length === 0) { + return of(EMPTY_RESPONSE); + } + + const filter = request.query?.bool?.filter + ? Array.isArray(request.query?.bool?.filter) + ? request.query?.bool?.filter + : [request.query?.bool?.filter] + : []; + if (authzFilter) { + filter.push(authzFilter); + } + if (space?.id) { + filter.push(getSpacesFilter(space.id) as estypes.QueryDslQueryContainer); + } + + const query = { + bool: { + ...request.query?.bool, + filter, + }, + }; + const params = { + index: indices, + body: { + _source: false, + fields: ['*'], + size: MAX_ALERT_SEARCH_SIZE, + query, + }, + }; + return es.search({ ...request, params }, options, deps); + }), + map((response) => { + // Do we have to loop over each hit? Yes. + // ecs auditLogger requires that we log each alert independently + if (securityAuditLogger != null) { + response.rawResponse.hits?.hits?.forEach((hit) => { + securityAuditLogger.log( + alertAuditEvent({ + action: AlertAuditAction.FIND, + id: hit._id, + outcome: 'success', + }) + ); + }); + } + return response; + }), + catchError((err) => { + // check if auth error, if yes, write to ecs logger + if (securityAuditLogger != null && err?.output?.statusCode === 403) { + securityAuditLogger.log( + alertAuditEvent({ + action: AlertAuditAction.FIND, + outcome: 'failure', + error: err, + }) + ); + } + + throw err; + }) + ); + }, + cancel: async (id, options, deps) => { + if (es.cancel) { + return es.cancel(id, options, deps); + } + }, + }; +}; diff --git a/x-pack/plugins/rule_registry/tsconfig.json b/x-pack/plugins/rule_registry/tsconfig.json index 384ffa0ee3428..810524a7a8122 100644 --- a/x-pack/plugins/rule_registry/tsconfig.json +++ b/x-pack/plugins/rule_registry/tsconfig.json @@ -19,6 +19,5 @@ { "path": "../../../src/plugins/data/tsconfig.json" }, { "path": "../alerting/tsconfig.json" }, { "path": "../security/tsconfig.json" }, - { "path": "../triggers_actions_ui/tsconfig.json" } ] } diff --git a/x-pack/plugins/triggers_actions_ui/tsconfig.json b/x-pack/plugins/triggers_actions_ui/tsconfig.json index ac36780f10c01..38d3fa9ad5996 100644 --- a/x-pack/plugins/triggers_actions_ui/tsconfig.json +++ b/x-pack/plugins/triggers_actions_ui/tsconfig.json @@ -17,6 +17,7 @@ { "path": "../../../src/core/tsconfig.json" }, { "path": "../alerting/tsconfig.json" }, { "path": "../features/tsconfig.json" }, + { "path": "../rule_registry/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json" }, { "path": "../../../src/plugins/saved_objects/tsconfig.json" }, { "path": "../../../src/plugins/home/tsconfig.json" }, diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts index e4512798db7d3..229f31375200a 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts @@ -10,8 +10,7 @@ import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/ // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { - // FAILING: https://github.com/elastic/kibana/issues/110153 - describe.skip('rules security and spaces enabled: basic', function () { + describe('rules security and spaces enabled: basic', function () { // Fastest ciGroup for the moment. this.tags('ciGroup5'); @@ -24,10 +23,12 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { }); // Basic - loadTestFile(require.resolve('./get_alert_by_id')); - loadTestFile(require.resolve('./update_alert')); - loadTestFile(require.resolve('./bulk_update_alerts')); - loadTestFile(require.resolve('./find_alerts')); - loadTestFile(require.resolve('./get_alerts_index')); + // FAILING: https://github.com/elastic/kibana/issues/110153 + // loadTestFile(require.resolve('./get_alert_by_id')); + // loadTestFile(require.resolve('./update_alert')); + // loadTestFile(require.resolve('./bulk_update_alerts')); + // loadTestFile(require.resolve('./find_alerts')); + // loadTestFile(require.resolve('./get_alerts_index')); + loadTestFile(require.resolve('./search_strategy')); }); }; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts new file mode 100644 index 0000000000000..2124cb8a1d04b --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts @@ -0,0 +1,138 @@ +/* + * 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 expect from '@kbn/expect'; +import { AlertConsumers } from '@kbn/rule-data-utils'; + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { RuleRegistrySearchResponse } from '../../../../../plugins/rule_registry/common/search_strategy'; +import { + deleteSignalsIndex, + createSignalsIndex, + deleteAllAlerts, + getRuleForSignalTesting, + createRule, + waitForSignalsToBePresent, + waitForRuleSuccessOrStatus, +} from '../../../../detection_engine_api_integration/utils'; +import { ID } from '../../../../detection_engine_api_integration/security_and_spaces/tests/generating_signals'; +import { QueryCreateSchema } from '../../../../../plugins/security_solution/common/detection_engine/schemas/request'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + const bsearch = getService('bsearch'); + const log = getService('log'); + + const SPACE1 = 'space1'; + + describe('ruleRegistryAlertsSearchStrategy', () => { + describe('logs', () => { + beforeEach(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); + }); + afterEach(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); + }); + it('should return alerts from log rules', async () => { + const result = await bsearch.send({ + supertest, + options: { + featureIds: [AlertConsumers.LOGS], + }, + strategy: 'ruleRegistryAlertsSearchStrategy', + }); + expect(result.rawResponse.hits.total).to.eql(5); + const consumers = result.rawResponse.hits.hits.map((hit) => { + return hit.fields?.['kibana.alert.rule.consumer']; + }); + expect(consumers.every((consumer) => consumer === AlertConsumers.LOGS)); + }); + }); + + describe('siem', () => { + beforeEach(async () => { + await deleteSignalsIndex(supertest, log); + await createSignalsIndex(supertest, log); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + }); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); + }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); + }); + + it('should return alerts from siem rules', async () => { + const rule: QueryCreateSchema = { + ...getRuleForSignalTesting(['auditbeat-*']), + query: `_id:${ID}`, + }; + const { id: createdId } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, createdId); + await waitForSignalsToBePresent(supertest, log, 1, [createdId]); + + const result = await bsearch.send({ + supertest, + options: { + featureIds: [AlertConsumers.SIEM], + }, + strategy: 'ruleRegistryAlertsSearchStrategy', + }); + expect(result.rawResponse.hits.total).to.eql(1); + const consumers = result.rawResponse.hits.hits.map( + (hit) => hit.fields?.['kibana.alert.rule.consumer'] + ); + expect(consumers.every((consumer) => consumer === AlertConsumers.SIEM)); + }); + }); + + describe('apm', () => { + beforeEach(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + afterEach(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + it('should return alerts from apm rules', async () => { + const result = await bsearch.send({ + supertest, + options: { + featureIds: [AlertConsumers.APM], + }, + strategy: 'ruleRegistryAlertsSearchStrategy', + space: SPACE1, + }); + expect(result.rawResponse.hits.total).to.eql(2); + const consumers = result.rawResponse.hits.hits.map( + (hit) => hit.fields?.['kibana.alert.rule.consumer'] + ); + expect(consumers.every((consumer) => consumer === AlertConsumers.APM)); + }); + }); + + describe('empty response', () => { + it('should return an empty response', async () => { + const result = await bsearch.send({ + supertest, + options: { + featureIds: [], + }, + strategy: 'ruleRegistryAlertsSearchStrategy', + space: SPACE1, + }); + expect(result.rawResponse).to.eql({}); + }); + }); + }); +}; From d499ed533a5d744cabea76a3347b0a99d72de7a4 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Sun, 27 Feb 2022 00:54:58 -0500 Subject: [PATCH 2/3] skip failing test suite (#126421) --- test/functional/apps/console/_autocomplete.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/console/_autocomplete.ts b/test/functional/apps/console/_autocomplete.ts index 423440ecdf3f8..580847351be9c 100644 --- a/test/functional/apps/console/_autocomplete.ts +++ b/test/functional/apps/console/_autocomplete.ts @@ -13,7 +13,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const PageObjects = getPageObjects(['common', 'console']); - describe('console autocomplete feature', function describeIndexTests() { + // Failing: See https://github.com/elastic/kibana/issues/126421 + describe.skip('console autocomplete feature', function describeIndexTests() { this.tags('includeFirefox'); before(async () => { log.debug('navigateTo console'); From e7e9be9fa69e6d96b54aeaf1b2457247001972f0 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 28 Feb 2022 09:49:55 +0100 Subject: [PATCH 3/3] [Discover] Fix ""range filter on version" permissions for cloud functional testing (#126377) --- test/functional/config.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/functional/config.js b/test/functional/config.js index 09eccc863a0e5..389f432641acf 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -201,6 +201,21 @@ export default async function ({ readConfigFile }) { kibana: [], }, + version_test: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['version-test'], + privileges: ['read', 'view_index_metadata', 'manage', 'create_index', 'index'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + kibana_sample_read: { elasticsearch: { cluster: [],