diff --git a/x-pack/plugins/rule_registry/server/utils/rbac.ts b/packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts similarity index 65% rename from x-pack/plugins/rule_registry/server/utils/rbac.ts rename to packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts index 172201400606a..2d0b0ec4a726c 100644 --- a/x-pack/plugins/rule_registry/server/utils/rbac.ts +++ b/packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts @@ -1,8 +1,9 @@ /* * 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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ /** @@ -13,7 +14,18 @@ * This doesn't work in combination with the `xpack.ruleRegistry.index` * setting, with which the user can change the index prefix. */ -export const mapConsumerToIndexName = { + +export const ALERTS_CONSUMERS = { + APM: 'apm', + LOGS: 'logs', + INFRASTRUCTURE: 'infrastructure', + OBSERVABILITY: 'observability', + SIEM: 'siem', + SYNTHETICS: 'synthetics', +} as const; +export type ALERTS_CONSUMERS = typeof ALERTS_CONSUMERS[keyof typeof ALERTS_CONSUMERS]; + +export const mapConsumerToIndexName: Record = { apm: '.alerts-observability-apm', logs: '.alerts-observability.logs', infrastructure: '.alerts-observability.metrics', diff --git a/packages/kbn-rule-data-utils/src/index.ts b/packages/kbn-rule-data-utils/src/index.ts index 93a2538c7aa2c..f60ad31286c9c 100644 --- a/packages/kbn-rule-data-utils/src/index.ts +++ b/packages/kbn-rule-data-utils/src/index.ts @@ -7,3 +7,4 @@ */ export * from './technical_field_names'; +export * from './alerts_as_data_rbac'; diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts index 248487f216a56..f6001739c856b 100644 --- a/src/plugins/data/server/search/mocks.ts +++ b/src/plugins/data/server/search/mocks.ts @@ -23,6 +23,7 @@ export function createSearchSetupMock(): jest.Mocked { export function createSearchStartMock(): jest.Mocked { return { aggs: searchAggsStartMock(), + searchAsInternalUser: jest.fn().mockReturnValue(createSearchRequestHandlerContext()), getSearchStrategy: jest.fn(), asScoped: jest.fn().mockReturnValue(createSearchRequestHandlerContext()), searchSource: searchSourceMock.createStartContract(), diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 46c15e6d44638..c475a50039d96 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -109,6 +109,7 @@ export class SearchService implements Plugin { private searchStrategies: StrategyMap = {}; private sessionService: ISearchSessionService; private asScoped!: ISearchStart['asScoped']; + private searchAsInternalUser!: ISearchStrategy; constructor( private initializerContext: PluginInitializerContext, diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index b3cd47d47dbc7..7bf908c02a5cb 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -102,8 +102,6 @@ export class AlertingAuthorization { // manually authorize each rule type in the management UI. this.exemptConsumerIds = exemptConsumerIds; - this.spaceId = getSpace(request).then((maybeSpace) => maybeSpace?.id); - this.featuresIds = getSpace(request) .then((maybeSpace) => new Set(maybeSpace?.disabledFeatures ?? [])) .then( @@ -127,6 +125,12 @@ export class AlertingAuthorization { return new Set(); }); + this.spaceId = getSpace(request) + .then((maybeSpace) => maybeSpace?.id) + .catch((e) => { + return undefined; + }); + this.allPossibleConsumers = this.featuresIds.then((featuresIds) => { return featuresIds.size ? asAuthorizedConsumers([...this.exemptConsumerIds, ...featuresIds], { @@ -302,8 +306,11 @@ export class AlertingAuthorization { ); const authorizedEntries: Map> = new Map(); + const alertsSpaceId = + authorizationEntity === AlertingAuthorizationEntity.Alert ? await this.spaceId : undefined; + return { - filter: asFiltersByRuleTypeAndConsumer(authorizedRuleTypes, filterOpts), + filter: asFiltersByRuleTypeAndConsumer(authorizedRuleTypes, filterOpts, alertsSpaceId), ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, authType: string) => { if (!authorizedRuleTypeIdsToConsumers.has(`${ruleTypeId}/${consumer}/${authType}`)) { throw Boom.forbidden( diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts index 98a156b54a452..a2e7487043647 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { remove } from 'lodash'; import { JsonObject } from '@kbn/common-utils'; -import { nodeBuilder, EsQueryConfig } from '../../../../../src/plugins/data/common'; -import { toElasticsearchQuery } from '../../../../../src/plugins/data/common/es_query'; +import { remove } from 'lodash'; + +import { nodeBuilder } from '../../../../../src/plugins/data/common'; import { KueryNode } from '../../../../../src/plugins/data/server'; import { RegistryAlertTypeWithAuth } from './alerting_authorization'; @@ -25,15 +25,9 @@ export interface AlertingAuthorizationFilterOpts { interface AlertingAuthorizationFilterFieldNames { ruleTypeId: string; consumer: string; + spaceIds?: string; } -const esQueryConfig: EsQueryConfig = { - allowLeadingWildcards: true, - dateFormatTZ: 'Zulu', - ignoreFilterIfFieldNotInIndex: false, - queryStringOptions: { analyze_wildcard: true }, -}; - export function asFiltersByRuleTypeAndConsumer( ruleTypes: Set, opts: AlertingAuthorizationFilterOpts, @@ -82,8 +76,8 @@ export const buildRuleTypeFilter = ( ruleTypes: Set, opts: AlertingAuthorizationFilterOpts, alertSpaceId?: string -) => { - const allFilters = Array.from(ruleTypes).map(({ id, authorizedConsumers }) => { +): JsonObject => { + const allFilters = Array.from(ruleTypes).map(({ id, authorizedConsumers }) => { const ruleIdFilter = { bool: { should: [ @@ -97,7 +91,7 @@ export const buildRuleTypeFilter = ( }, }; const spaceIdFilter = - alertSpaceId != null + alertSpaceId != null && opts.fieldNames.spaceIds != null ? { bool: { filter: [{ term: { [opts.fieldNames.spaceIds]: alertSpaceId } }], 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 e5b89cb86acf0..8f67c31dcb82c 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 @@ -6,6 +6,11 @@ */ import { PublicMethodsOf } from '@kbn/utility-types'; import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils'; +import { + mapConsumerToIndexName, + validFeatureIds, + isValidFeatureId, +} from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; import { AlertTypeParams } from '../../../alerting/server'; import { @@ -24,7 +29,6 @@ import { SPACE_IDS, } from '../../common/technical_rule_data_field_names'; import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; -import { mapConsumerToIndexName, validFeatureIds, isValidFeatureId } from '../utils/rbac'; // TODO: Fix typings https://github.com/elastic/kibana/issues/101776 type NonNullableProps = Omit & diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts index b8b181a493cec..3e3bde7429fe0 100644 --- a/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts @@ -8,10 +8,10 @@ import { IRouter } from 'kibana/server'; import { id as _id } from '@kbn/securitysolution-io-ts-list-types'; import { transformError } from '@kbn/securitysolution-es-utils'; +import { validFeatureIds } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; import { RacRequestHandlerContext } from '../types'; import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; -import { validFeatureIds } from '../utils/rbac'; export const getAlertsIndexRoute = (router: IRouter) => { router.get( diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts index 92ba5c7060ebb..277121074f7f2 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts @@ -7,11 +7,12 @@ import { ApiResponse } from '@elastic/elasticsearch'; import { BulkRequest, BulkResponse } from '@elastic/elasticsearch/api/types'; +import { ValidFeatureId } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; + import { ElasticsearchClient } from 'kibana/server'; import { FieldDescriptor } from 'src/plugins/data/server'; import { ESSearchRequest, ESSearchResponse } from 'src/core/types/elasticsearch'; import { TechnicalRuleDataFieldName } from '../../common/technical_rule_data_field_names'; -import { ValidFeatureId } from '../utils/rbac'; export interface RuleDataReader { search( diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts index d84f85dbc99b7..6ca12042a47ce 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts @@ -6,6 +6,8 @@ */ import { ClusterPutComponentTemplate } from '@elastic/elasticsearch/api/requestParams'; import { estypes } from '@elastic/elasticsearch'; +import { ValidFeatureId } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; + import { ElasticsearchClient, Logger } from 'kibana/server'; import { get, isEmpty } from 'lodash'; import { technicalComponentTemplate } from '../../common/assets/component_templates/technical_component_template'; @@ -20,7 +22,6 @@ import { ClusterPutComponentTemplateBody, PutIndexTemplateRequest } from '../../ import { RuleDataClient } from '../rule_data_client'; import { RuleDataWriteDisabledError } from './errors'; import { incrementIndexName } from './utils'; -import { ValidFeatureId } from '../utils/rbac'; const BOOTSTRAP_TIMEOUT = 60000; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 07b0e2ed4b9dd..853251bd12cd4 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -26,6 +26,8 @@ import { PluginSetupContract as AlertingSetup, PluginStartContract as AlertPluginStartContract, } from '../../alerting/server'; +import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map'; +import { technicalRuleFieldMap } from '../../rule_registry/common/assets/field_maps/technical_rule_field_map'; import { PluginStartContract as CasesPluginStartContract } from '../../cases/server'; import { @@ -67,6 +69,7 @@ import { NOTIFICATIONS_ID, REFERENCE_RULE_ALERT_TYPE_ID, REFERENCE_RULE_PERSISTENCE_ALERT_TYPE_ID, + CUSTOM_ALERT_TYPE_ID, } from '../common/constants'; import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerLimitedConcurrencyRoutes } from './endpoint/routes/limited_concurrency'; @@ -212,7 +215,7 @@ export class Plugin implements IPlugin extends SortField { diff --git a/x-pack/plugins/timelines/kibana.json b/x-pack/plugins/timelines/kibana.json index 5cc05a5996f74..17e957d778316 100644 --- a/x-pack/plugins/timelines/kibana.json +++ b/x-pack/plugins/timelines/kibana.json @@ -6,6 +6,6 @@ "extraPublicDirs": ["common"], "server": true, "ui": true, - "requiredPlugins": ["data", "dataEnhanced", "kibanaReact", "kibanaUtils"], + "requiredPlugins": ["alerting", "data", "dataEnhanced", "kibanaReact", "kibanaUtils"], "optionalPlugins": [] } diff --git a/x-pack/plugins/timelines/server/plugin.ts b/x-pack/plugins/timelines/server/plugin.ts index 78e91f965e751..82f610fee632d 100644 --- a/x-pack/plugins/timelines/server/plugin.ts +++ b/x-pack/plugins/timelines/server/plugin.ts @@ -36,7 +36,10 @@ export class TimelinesPlugin // Register search strategy core.getStartServices().then(([_, depsStart]) => { - const TimelineSearchStrategy = timelineSearchStrategyProvider(depsStart.data); + const TimelineSearchStrategy = timelineSearchStrategyProvider( + depsStart.data, + depsStart.alerting + ); const TimelineEqlSearchStrategy = timelineEqlSearchStrategyProvider(depsStart.data); const IndexFields = indexFieldsProvider(); diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts index c1b567b99cfb1..026c980391888 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts @@ -22,13 +22,13 @@ import { buildFieldsRequest, formatTimelineData } from './helpers'; import { inspectStringifyObject } from '../../../../../utils/build_query'; export const timelineEventsAll: TimelineFactory = { - buildDsl: (options: TimelineEventsAllRequestOptions) => { + buildDsl: ({ authFilter, ...options }: TimelineEventsAllRequestOptions) => { if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); } const { fieldRequested, ...queryOptions } = cloneDeep(options); queryOptions.fields = buildFieldsRequest(fieldRequested, queryOptions.excludeEcsData); - return buildTimelineEventsAllQuery(queryOptions); + return buildTimelineEventsAllQuery({ ...queryOptions, authFilter }); }, parse: async ( options: TimelineEventsAllRequestOptions, diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts index 40df5376cefc9..321301b56ed0f 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts @@ -23,6 +23,7 @@ export const buildTimelineEventsAllQuery = ({ pagination: { activePage, querySize }, sort, timerange, + authFilter, }: Omit) => { const filterClause = [...createQueryFilterClauses(filterQuery)]; @@ -46,7 +47,8 @@ export const buildTimelineEventsAllQuery = ({ return []; }; - const filter = [...filterClause, ...getTimerangeFilter(timerange), { match_all: {} }]; + const filters = [...filterClause, ...getTimerangeFilter(timerange), { match_all: {} }]; + const filter = authFilter != null ? [...filters, { ...authFilter }] : filters; const getSortField = (sortFields: TimelineRequestSortField[]) => sortFields.map((item) => { diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts index dd46c0496df64..cc77b3afd3257 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts @@ -5,48 +5,165 @@ * 2.0. */ -import { map, mergeMap } from 'rxjs/operators'; +import { map, mergeMap, catchError } from 'rxjs/operators'; +import { from } from 'rxjs'; +import { + isValidFeatureId, + mapConsumerToIndexName, + ALERTS_CONSUMERS, +} from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; + +import { + AlertingAuthorizationEntity, + AlertingAuthorizationFilterType, + PluginStartContract as AlertPluginStartContract, +} from '../../../../alerting/server'; import { ISearchStrategy, PluginStart, + SearchStrategyDependencies, shimHitsTotal, } from '../../../../../../src/plugins/data/server'; -import { ENHANCED_ES_SEARCH_STRATEGY } from '../../../../../../src/plugins/data/common'; import { TimelineFactoryQueryTypes, TimelineStrategyResponseType, TimelineStrategyRequestType, + EntityType, } from '../../../common/search_strategy/timeline'; import { timelineFactory } from './factory'; import { TimelineFactory } from './factory/types'; +import { + ENHANCED_ES_SEARCH_STRATEGY, + ISearchOptions, +} from '../../../../../../src/plugins/data/common'; export const timelineSearchStrategyProvider = ( - data: PluginStart + data: PluginStart, + alerting: AlertPluginStartContract ): ISearchStrategy, TimelineStrategyResponseType> => { + const esAsInternal = data.search.searchAsInternalUser; const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); + return { search: (request, options, deps) => { - if (request.factoryQueryType == null) { + const factoryQueryType = request.factoryQueryType; + const entityType = request.entityType; + const alertConsumers = request.alertConsumers; + + if (factoryQueryType == null) { throw new Error('factoryQueryType is required'); } - const queryFactory: TimelineFactory = timelineFactory[request.factoryQueryType]; - const dsl = queryFactory.buildDsl(request); - return es.search({ ...request, params: dsl }, options, deps).pipe( - map((response) => { - return { - ...response, - ...{ - rawResponse: shimHitsTotal(response.rawResponse, options), - }, - }; - }), - mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes)) - ); + + const queryFactory: TimelineFactory = timelineFactory[factoryQueryType]; + + if (entityType != null && entityType === EntityType.ALERTS) { + return timelineAlertsSearchStrategy({ + es: esAsInternal, + request, + options, + deps, + queryFactory, + alerting, + alertConsumers: alertConsumers ?? [], + }); + } else { + return timelineSearchStrategy({ es, request, options, deps, queryFactory }); + } }, cancel: async (id, options, deps) => { if (es.cancel) { return es.cancel(id, options, deps); + } else if (esAsInternal.cancel) { + return esAsInternal.cancel(id, options, deps); } }, }; }; + +const timelineSearchStrategy = ({ + es, + request, + options, + deps, + queryFactory, +}: { + es: ISearchStrategy; + request: TimelineStrategyRequestType; + options: ISearchOptions; + deps: SearchStrategyDependencies; + queryFactory: TimelineFactory; +}) => { + const { entityType, alertConsumers, ...rest } = request; + const dsl = queryFactory.buildDsl(request); + return es.search({ request: { ...rest }, params: dsl }, options, deps).pipe( + map((response) => { + return { + ...response, + ...{ + rawResponse: shimHitsTotal(response.rawResponse, options), + }, + }; + }), + mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes)) + ); +}; + +const timelineAlertsSearchStrategy = ({ + es, + request, + options, + deps, + queryFactory, + alerting, + alertConsumers, +}: { + es: ISearchStrategy; + request: TimelineStrategyRequestType; + options: ISearchOptions; + deps: SearchStrategyDependencies; + alerting: AlertPluginStartContract; + queryFactory: TimelineFactory; + alertConsumers: ALERTS_CONSUMERS[]; +}) => { + const allFeatureIdsValid = alertConsumers.every((id) => isValidFeatureId(id)); + + if (!allFeatureIdsValid) { + throw new Error('An invalid alerts consumer feature id was provided'); + } + + const indices = alertConsumers.flatMap((consumer) => mapConsumerToIndexName[consumer]); + const requestWithAlertsIndices = { ...request, defaultIndex: indices }; + + // Note: Alerts RBAC are built off of the alerting's authorization class, which + // is why we are pulling from alerting, not ther alertsClient here + const alertingAuthorizationClient = alerting.getAlertingAuthorizationWithRequest(deps.request); + + const getAuthFilter = async () => + alertingAuthorizationClient.getFindAuthorizationFilter(AlertingAuthorizationEntity.Alert, { + type: AlertingAuthorizationFilterType.ESDSL, + fieldNames: { + consumer: 'kibana.rac.alert.owner', + ruleTypeId: 'rule.id', + spaceIds: 'kibana.space_ids', + }, + }); + + return from(getAuthFilter()).pipe( + mergeMap(({ filter }) => { + const dsl = queryFactory.buildDsl({ ...requestWithAlertsIndices, authFilter: filter }); + return es.search({ ...requestWithAlertsIndices, params: dsl }, options, deps); + }), + map((response) => { + return { + ...response, + ...{ + rawResponse: shimHitsTotal(response.rawResponse, options), + }, + }; + }), + mergeMap((esSearchRes) => queryFactory.parse(requestWithAlertsIndices, esSearchRes)), + catchError((err) => { + throw err; + }) + ); +}; diff --git a/x-pack/plugins/timelines/server/types.ts b/x-pack/plugins/timelines/server/types.ts index 9ea4ef430d8fd..f8a2a794d9922 100644 --- a/x-pack/plugins/timelines/server/types.ts +++ b/x-pack/plugins/timelines/server/types.ts @@ -7,6 +7,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DataPluginSetup, DataPluginStart } from '../../../../src/plugins/data/server/plugin'; +import { PluginStartContract as AlertPluginStartContract } from '../../alerting/server'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface TimelinesPluginUI {} @@ -19,4 +20,5 @@ export interface SetupPlugins { export interface StartPlugins { data: DataPluginStart; + alerting: AlertPluginStartContract; } diff --git a/x-pack/plugins/timelines/tsconfig.json b/x-pack/plugins/timelines/tsconfig.json index 1bc60a696fcef..9662a59b8f240 100644 --- a/x-pack/plugins/timelines/tsconfig.json +++ b/x-pack/plugins/timelines/tsconfig.json @@ -24,6 +24,7 @@ { "path": "../data_enhanced/tsconfig.json" }, { "path": "../features/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, - { "path": "../spaces/tsconfig.json" } + { "path": "../spaces/tsconfig.json" }, + { "path": "../alerting/tsconfig.json" } ] } diff --git a/x-pack/test/api_integration/apis/security_solution/events.ts b/x-pack/test/api_integration/apis/security_solution/events.ts index 2150b022ac425..f85a4e6319f36 100644 --- a/x-pack/test/api_integration/apis/security_solution/events.ts +++ b/x-pack/test/api_integration/apis/security_solution/events.ts @@ -6,8 +6,30 @@ */ import expect from '@kbn/expect'; +import { JsonObject } from '@kbn/common-utils'; -import { secOnly } from '../../../rule_registry/common/lib/authentication/users'; +import { User } from '../../../rule_registry/common/lib/authentication/types'; +import { EntityType } from '../../../../plugins/timelines/common'; +import { + superUser, + globalRead, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + secOnlySpace2, + secOnlyReadSpace2, + obsSecAllSpace2, + obsSecReadSpace2, + obsOnlySpace2, + obsOnlyReadSpace2, + obsOnlySpacesAll, + obsSecSpacesAll, + secOnlySpacesAll, + noKibanaPrivileges, +} from '../../../rule_registry/common/lib/authentication/users'; import { createSpacesAndUsers, deleteSpacesAndUsers, @@ -17,10 +39,30 @@ import { TimelineEventsQueries, } from '../../../../plugins/security_solution/common/search_strategy'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { getDocValueFields, getFieldsToRequest, getFilterValue } from './utils'; + +interface TestCase { + /** The space where the alert exists */ + space?: string; + /** The ID of the solution for which to get alerts */ + featureIds: string[]; + /** The total alerts expected to be returned */ + expectedNumberAlerts: number; + /** body to be posted */ + body: JsonObject; + /** Authorized users */ + authorizedUsers: User[]; + /** Unauthorized users */ + unauthorizedUsers: User[]; + /** Users who are authorized for one, but not all of the alert solutions being queried */ + usersWithoutAllPrivileges?: User[]; +} const TO = '3000-01-01T00:00:00.000Z'; const FROM = '2000-01-01T00:00:00.000Z'; - +const TEST_URL = '/internal/search/timelineSearchStrategy/'; +const SPACE_1 = 'space1'; +const SPACE_2 = 'space2'; // typical values that have to change after an update from "scripts/es_archiver" const DATA_COUNT = 7; const HOST_NAME = 'suricata-sensor-amsterdam'; @@ -30,389 +72,40 @@ const ACTIVE_PAGE = 0; const PAGE_SIZE = 25; const LIMITED_PAGE_SIZE = 2; -const FILTER_VALUE = { - bool: { - filter: [ - { - bool: { - should: [{ match_phrase: { 'host.name': HOST_NAME } }], - minimum_should_match: 1, - }, - }, - { - bool: { - filter: [ - { - bool: { - should: [{ range: { '@timestamp': { gte: FROM } } }], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [{ range: { '@timestamp': { lte: TO } } }], - minimum_should_match: 1, - }, - }, - ], - }, - }, - ], - }, +const getSpaceUrlPrefix = (spaceId?: string) => { + return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; }; -/** - * https://www.elastic.co/guide/en/elasticsearch/reference/7.12/search-fields.html#docvalue-fields - * Use the docvalue_fields parameter to get values for selected fields. - * This can be a good choice when returning a fairly small number of fields that support doc values, - * such as keywords and dates. - */ -const DOC_VALUE_FIELDS = [ - { - field: '@timestamp', - }, - { - field: 'agent.ephemeral_id', - }, - { - field: 'agent.id', - }, - { - field: 'agent.name', - }, - { - field: 'agent.type', - }, - { - field: 'agent.version', - }, - { - field: 'as.number', - }, - { - field: 'as.organization.name', - }, - { - field: 'client.address', - }, - { - field: 'client.as.number', - }, - { - field: 'client.as.organization.name', - }, - { - field: 'client.bytes', - format: 'bytes', - }, - { - field: 'client.domain', - }, - { - field: 'client.geo.city_name', - }, - { - field: 'client.geo.continent_name', - }, - { - field: 'client.geo.country_iso_code', - }, - { - field: 'client.geo.country_name', - }, - { - field: 'client.geo.location', - }, - { - field: 'client.geo.name', - }, - { - field: 'client.geo.region_iso_code', - }, - { - field: 'client.geo.region_name', - }, - { - field: 'client.ip', - }, - { - field: 'client.mac', - }, - { - field: 'client.nat.ip', - }, - { - field: 'client.nat.port', - format: 'string', - }, - { - field: 'client.packets', - }, - { - field: 'client.port', - format: 'string', - }, - { - field: 'client.registered_domain', - }, - { - field: 'client.top_level_domain', - }, - { - field: 'client.user.domain', - }, - { - field: 'client.user.email', - }, - { - field: 'client.user.full_name', - }, - { - field: 'client.user.group.domain', - }, - { - field: 'client.user.group.id', - }, - { - field: 'client.user.group.name', - }, - { - field: 'client.user.hash', - }, - { - field: 'client.user.id', - }, - { - field: 'client.user.name', - }, - { - field: 'cloud.account.id', - }, - { - field: 'cloud.availability_zone', - }, - { - field: 'cloud.instance.id', - }, - { - field: 'cloud.instance.name', - }, - { - field: 'cloud.machine.type', - }, - { - field: 'cloud.provider', - }, - { - field: 'cloud.region', - }, - { - field: 'code_signature.exists', - }, - { - field: 'code_signature.status', - }, - { - field: 'code_signature.subject_name', - }, - { - field: 'code_signature.trusted', - }, - { - field: 'code_signature.valid', - }, - { - field: 'container.id', - }, - { - field: 'container.image.name', - }, - { - field: 'container.image.tag', - }, - { - field: 'container.name', - }, - { - field: 'container.runtime', - }, - { - field: 'destination.address', - }, - { - field: 'destination.as.number', - }, - { - field: 'destination.as.organization.name', - }, - { - field: 'destination.bytes', - format: 'bytes', - }, - { - field: 'destination.domain', - }, - { - field: 'destination.geo.city_name', - }, - { - field: 'destination.geo.continent_name', - }, - { - field: 'destination.geo.country_iso_code', - }, - { - field: 'destination.geo.country_name', - }, - { - field: 'destination.geo.location', - }, - { - field: 'destination.geo.name', - }, - { - field: 'destination.geo.region_iso_code', - }, - { - field: 'destination.geo.region_name', - }, - { - field: 'destination.ip', - }, - { - field: 'destination.mac', - }, - { - field: 'destination.nat.ip', - }, - { - field: 'destination.nat.port', - format: 'string', - }, - { - field: 'destination.packets', - }, - { - field: 'destination.port', - format: 'string', - }, - { - field: 'destination.registered_domain', - }, - { - field: 'destination.top_level_domain', - }, - { - field: 'destination.user.domain', - }, - { - field: 'destination.user.email', - }, - { - field: 'destination.user.full_name', - }, - { - field: 'destination.user.group.domain', - }, - { - field: 'destination.user.group.id', - }, - { - field: 'destination.user.group.name', - }, - { - field: 'destination.user.hash', - }, - { - field: 'destination.user.id', - }, - { - field: 'destination.user.name', - }, - { - field: 'dll.code_signature.exists', - }, - { - field: 'dll.code_signature.status', - }, - { - field: 'dll.code_signature.subject_name', - }, - { - field: 'dll.code_signature.trusted', - }, - { - field: 'dll.code_signature.valid', - }, - { - field: 'dll.hash.md5', - }, - { - field: 'dll.hash.sha1', - }, - { - field: 'dll.hash.sha256', - }, - { - field: 'dll.hash.sha512', - }, - { - field: 'dll.name', - }, - { - field: 'dll.path', - }, - { - field: 'dll.pe.company', - }, - { - field: 'dll.pe.description', - }, - { - field: 'dll.pe.file_version', - }, - { - field: 'dll.pe.original_file_name', - }, -]; -const FIELD_REQUESTED = [ - '@timestamp', - 'message', - 'event.category', - 'event.action', - 'host.name', - 'source.ip', - 'destination.ip', - 'user.name', - '@timestamp', - 'signal.status', - 'signal.group.id', - 'signal.original_time', - 'signal.rule.building_block_type', - 'signal.rule.filters', - 'signal.rule.from', - 'signal.rule.language', - 'signal.rule.query', - 'signal.rule.name', - 'signal.rule.to', - 'signal.rule.id', - 'signal.rule.index', - 'signal.rule.type', - 'signal.original_event.kind', - 'signal.original_event.module', - 'file.path', - 'file.Ext.code_signature.subject_name', - 'file.Ext.code_signature.trusted', - 'file.hash.sha256', - 'host.os.family', - 'event.code', -]; - export default function ({ getService }: FtrProviderContext) { - const retry = getService('retry'); const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); + const getPostBody = (): JsonObject => ({ + defaultIndex: ['auditbeat-*'], + docValueFields: getDocValueFields(), + factoryQueryType: TimelineEventsQueries.all, + entityType: EntityType.ALL, + fieldRequested: getFieldsToRequest(), + fields: [], + filterQuery: getFilterValue(HOST_NAME, FROM, TO), + pagination: { + activePage: 0, + querySize: 25, + }, + language: 'kuery', + sort: [ + { + field: '@timestamp', + direction: Direction.desc, + type: 'number', + }, + ], + timerange: { + from: FROM, + to: TO, + interval: '12h', + }, + }); describe('Timeline', () => { before(async () => { @@ -426,141 +119,401 @@ export default function ({ getService }: FtrProviderContext) { await deleteSpacesAndUsers(getService); }); - it('Make sure that we get Timeline data', async () => { - await retry.try(async () => { - const resp = await supertest - .post('/internal/search/timelineSearchStrategy/') - .set('kbn-xsrf', 'true') - .set('Content-Type', 'application/json') - .send({ - defaultIndex: ['auditbeat-*'], - docValueFields: DOC_VALUE_FIELDS, - factoryQueryType: TimelineEventsQueries.all, - fieldRequested: FIELD_REQUESTED, - fields: [], - filterQuery: FILTER_VALUE, - pagination: { - activePage: 0, - querySize: 25, - }, - language: 'kuery', - sort: [ - { - field: '@timestamp', - direction: Direction.desc, - type: 'number', - }, - ], - timerange: { - from: FROM, - to: TO, - interval: '12h', - }, - }) - .expect(200); + function addTests({ + space, + authorizedUsers, + usersWithoutAllPrivileges, + unauthorizedUsers, + body, + featureIds, + expectedNumberAlerts, + }: TestCase) { + authorizedUsers.forEach(({ username, password }) => { + it(`${username} should be able to view alerts from "${featureIds.join(',')}" ${ + space != null ? `in space ${space}` : 'when no space specified' + }`, async () => { + const resp = await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(space)}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send({ ...body }) + .expect(200); + + const timeline = resp.body; + + expect( + timeline.edges.every((hit) => { + const data = hit.node.data; + return data.some(({ field, value }) => { + return field === 'kibana.rac.alert.owner' && featureIds.includes(value[0]); + }); + }) + ).to.equal(true); + expect(timeline.totalCount).to.be(expectedNumberAlerts); + }); + }); + + if (usersWithoutAllPrivileges != null) { + usersWithoutAllPrivileges.forEach(({ username, password }) => { + it(`${username} should NOT be able to view alerts from "${featureIds.join(',')}" ${ + space != null ? `in space ${space}` : 'when no space specified' + }`, async () => { + const resp = await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(space)}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send({ ...body }) + .expect(200); + + const timeline = resp.body; + + expect(timeline.totalCount).to.be(0); + }); + }); + } - const timeline = resp.body; - expect(timeline.edges.length).to.be(EDGE_LENGTH); - expect(timeline.edges[0].node.data.length).to.be(DATA_COUNT); - expect(timeline.totalCount).to.be(TOTAL_COUNT); - expect(timeline.pageInfo.activePage).to.equal(ACTIVE_PAGE); - expect(timeline.pageInfo.querySize).to.equal(PAGE_SIZE); + unauthorizedUsers.forEach(({ username, password }) => { + it(`${username} should NOT be able to access "${featureIds.join(',')}" ${ + space != null ? `in space ${space}` : 'when no space specified' + }`, async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(space)}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send({ ...body }) + .expect(500); + }); }); + } + + it('returns Timeline data', async () => { + const resp = await supertest + .post(TEST_URL) + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send(getPostBody()) + .expect(200); + + const timeline = resp.body; + expect(timeline.edges.length).to.be(EDGE_LENGTH); + expect(timeline.edges[0].node.data.length).to.be(DATA_COUNT); + expect(timeline.totalCount).to.be(TOTAL_COUNT); + expect(timeline.pageInfo.activePage).to.equal(ACTIVE_PAGE); + expect(timeline.pageInfo.querySize).to.equal(PAGE_SIZE); }); - // TODO: unskip this test once authz is added to search strategy - it.skip('Make sure that we get Timeline data using the hunter role and do not receive observability alerts', async () => { - await retry.try(async () => { - const requestBody = { - defaultIndex: ['.alerts*'], // query both .alerts-observability-apm and .alerts-security-solution - docValueFields: [], - factoryQueryType: TimelineEventsQueries.all, - fieldRequested: FIELD_REQUESTED, - // fields: [], - filterQuery: { - bool: { - filter: [ - { - match_all: {}, - }, - ], - }, - }, + it('returns paginated Timeline query', async () => { + const resp = await supertest + .post(TEST_URL) + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send({ + ...getPostBody(), pagination: { activePage: 0, - querySize: 25, + querySize: LIMITED_PAGE_SIZE, }, - language: 'kuery', - sort: [ - { - field: '@timestamp', - direction: Direction.desc, - type: 'number', + }) + .expect(200); + + const timeline = resp.body; + expect(timeline.edges.length).to.be(LIMITED_PAGE_SIZE); + expect(timeline.edges[0].node.data.length).to.be(DATA_COUNT); + expect(timeline.totalCount).to.be(TOTAL_COUNT); + expect(timeline.edges[0].node.data.length).to.be(DATA_COUNT); + expect(timeline.edges[0]!.node.ecs.host!.name).to.eql([HOST_NAME]); + }); + + describe('alerts authentication', () => { + const authorizedSecSpace1 = [secOnly, secOnlyRead]; + const authorizedObsSpace1 = [obsOnly, obsOnlyRead]; + const authorizedSecObsSpace1 = [obsSec, obsSecRead]; + + const authorizedSecSpace2 = [secOnlySpace2, secOnlyReadSpace2]; + const authorizedObsSpace2 = [obsOnlySpace2, obsOnlyReadSpace2]; + const authorizedSecObsSpace2 = [obsSecAllSpace2, obsSecReadSpace2]; + + const authorizedSecInAllSpaces = [secOnlySpacesAll]; + const authorizedObsInAllSpaces = [obsOnlySpacesAll]; + const authorizedSecObsInAllSpaces = [obsSecSpacesAll]; + + const authorizedInAllSpaces = [superUser, globalRead]; + const unauthorized = [noKibanaPrivileges]; + + describe('Querying for Security Solution alerts only', () => { + addTests({ + space: SPACE_1, + featureIds: ['siem'], + expectedNumberAlerts: 1, + body: { + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: EntityType.ALERTS, + alertConsumers: ['siem'], + filterQuery: { + bool: { + filter: [ + { + match_all: {}, + }, + ], + }, }, - ], - timerange: { - from: FROM, - to: TO, - interval: '12h', }, - }; - const resp = await supertestWithoutAuth - .post('/internal/search/securitySolutionTimelineSearchStrategy/') - .auth(secOnly.username, secOnly.password) // using security 'hunter' role - .set('kbn-xsrf', 'true') - .set('Content-Type', 'application/json') - .send(requestBody) - .expect(200); + authorizedUsers: [ + ...authorizedSecSpace1, + ...authorizedSecObsSpace1, + ...authorizedSecInAllSpaces, + ...authorizedSecObsInAllSpaces, + ...authorizedInAllSpaces, + ], + usersWithoutAllPrivileges: [...authorizedObsSpace1, ...authorizedObsInAllSpaces], + unauthorizedUsers: [ + ...authorizedSecSpace2, + ...authorizedObsSpace2, + ...authorizedSecObsSpace2, + ...unauthorized, + ], + }); - const timeline = resp.body; + addTests({ + space: SPACE_2, + featureIds: ['siem'], + expectedNumberAlerts: 1, + body: { + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: EntityType.ALERTS, + alertConsumers: ['siem'], + filterQuery: { + bool: { + filter: [ + { + match_all: {}, + }, + ], + }, + }, + }, + authorizedUsers: [ + ...authorizedSecSpace2, + ...authorizedSecObsSpace2, + ...authorizedSecInAllSpaces, + ...authorizedSecObsInAllSpaces, + ...authorizedInAllSpaces, + ], + usersWithoutAllPrivileges: [...authorizedObsSpace2, ...authorizedObsInAllSpaces], + unauthorizedUsers: [ + ...authorizedSecSpace1, + ...authorizedObsSpace1, + ...authorizedSecObsSpace1, + ...unauthorized, + ], + }); + }); - // we inject one alert into the security solutions alerts index and another alert into the observability alerts index - // therefore when accessing the .alerts* index with the security solution user, - // only security solution alerts should be returned since the security solution user - // is not authorized to view observability alerts. - expect(timeline.totalCount).to.be(1); + describe('Querying for APM alerts only', () => { + addTests({ + space: SPACE_1, + featureIds: ['apm'], + expectedNumberAlerts: 2, + body: { + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: EntityType.ALERTS, + alertConsumers: ['apm'], + filterQuery: { + bool: { + filter: [ + { + match_all: {}, + }, + ], + }, + }, + }, + authorizedUsers: [ + ...authorizedObsSpace1, + ...authorizedSecObsSpace1, + ...authorizedObsInAllSpaces, + ...authorizedSecObsInAllSpaces, + ...authorizedInAllSpaces, + ], + usersWithoutAllPrivileges: [...authorizedSecSpace1, ...authorizedSecInAllSpaces], + unauthorizedUsers: [ + ...authorizedSecSpace2, + ...authorizedObsSpace2, + ...authorizedSecObsSpace2, + ...unauthorized, + ], + }); + addTests({ + space: SPACE_2, + featureIds: ['apm'], + expectedNumberAlerts: 2, + body: { + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: EntityType.ALERTS, + alertConsumers: ['apm'], + filterQuery: { + bool: { + filter: [ + { + match_all: {}, + }, + ], + }, + }, + }, + authorizedUsers: [ + ...authorizedObsSpace2, + ...authorizedSecObsSpace2, + ...authorizedObsInAllSpaces, + ...authorizedSecObsInAllSpaces, + ...authorizedInAllSpaces, + ], + usersWithoutAllPrivileges: [...authorizedSecSpace2, ...authorizedSecInAllSpaces], + unauthorizedUsers: [ + ...authorizedSecSpace1, + ...authorizedObsSpace1, + ...authorizedSecObsSpace1, + ...unauthorized, + ], + }); }); - }); - it('Make sure that pagination is working in Timeline query', async () => { - await retry.try(async () => { - const resp = await supertest - .post('/internal/search/timelineSearchStrategy/') - .set('kbn-xsrf', 'true') - .set('Content-Type', 'application/json') - .send({ - defaultIndex: ['auditbeat-*'], - docValueFields: DOC_VALUE_FIELDS, - factoryQueryType: TimelineEventsQueries.all, - fieldRequested: FIELD_REQUESTED, - fields: [], - filterQuery: FILTER_VALUE, - pagination: { - activePage: 0, - querySize: LIMITED_PAGE_SIZE, + describe('Querying for multiple solutions', () => { + describe('authorized for both security solution and apm', () => { + addTests({ + space: SPACE_1, + featureIds: ['siem', 'apm'], + expectedNumberAlerts: 3, + body: { + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: EntityType.ALERTS, + alertConsumers: ['siem', 'apm'], + filterQuery: { + bool: { + filter: [ + { + match_all: {}, + }, + ], + }, + }, }, - language: 'kuery', - sort: [ - { - field: '@timestamp', - direction: Direction.desc, - type: 'number', + authorizedUsers: [ + ...authorizedSecObsSpace1, + ...authorizedSecObsInAllSpaces, + ...authorizedInAllSpaces, + ], + unauthorizedUsers: [...unauthorized], + }); + addTests({ + space: SPACE_2, + featureIds: ['siem', 'apm'], + expectedNumberAlerts: 3, + body: { + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: EntityType.ALERTS, + alertConsumers: ['siem', 'apm'], + filterQuery: { + bool: { + filter: [ + { + match_all: {}, + }, + ], + }, }, + }, + authorizedUsers: [ + ...authorizedSecObsSpace2, + ...authorizedSecObsInAllSpaces, + ...authorizedInAllSpaces, ], - timerange: { - from: FROM, - to: TO, - interval: '12h', + unauthorizedUsers: [...unauthorized], + }); + }); + describe('security solution privileges only', () => { + addTests({ + space: SPACE_1, + featureIds: ['siem'], + expectedNumberAlerts: 1, + body: { + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: EntityType.ALERTS, + alertConsumers: ['siem', 'apm'], + filterQuery: { + bool: { + filter: [ + { + match_all: {}, + }, + ], + }, + }, + }, + authorizedUsers: [...authorizedSecInAllSpaces], + unauthorizedUsers: [...unauthorized], + }); + }); + + describe('apm privileges only', () => { + addTests({ + space: SPACE_1, + featureIds: ['apm'], + expectedNumberAlerts: 2, + body: { + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: EntityType.ALERTS, + alertConsumers: ['siem', 'apm'], + filterQuery: { + bool: { + filter: [ + { + match_all: {}, + }, + ], + }, + }, }, - }) - .expect(200); + authorizedUsers: [...authorizedObsInAllSpaces], + unauthorizedUsers: [...unauthorized], + }); + }); - const timeline = resp.body; - expect(timeline.edges.length).to.be(LIMITED_PAGE_SIZE); - expect(timeline.edges[0].node.data.length).to.be(DATA_COUNT); - expect(timeline.totalCount).to.be(TOTAL_COUNT); - expect(timeline.edges[0].node.data.length).to.be(DATA_COUNT); - expect(timeline.edges[0]!.node.ecs.host!.name).to.eql([HOST_NAME]); + describe('querying from default space when no alerts were created in default space', () => { + addTests({ + featureIds: ['siem'], + expectedNumberAlerts: 0, + body: { + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: EntityType.ALERTS, + alertConsumers: ['siem', 'apm'], + filterQuery: { + bool: { + filter: [ + { + match_all: {}, + }, + ], + }, + }, + }, + authorizedUsers: [...authorizedSecInAllSpaces], + unauthorizedUsers: [...unauthorized], + }); + }); }); }); }); diff --git a/x-pack/test/api_integration/apis/security_solution/utils.ts b/x-pack/test/api_integration/apis/security_solution/utils.ts new file mode 100644 index 0000000000000..1327d6fa89bd1 --- /dev/null +++ b/x-pack/test/api_integration/apis/security_solution/utils.ts @@ -0,0 +1,398 @@ +/* + * 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 { JsonObject } from '@kbn/common-utils'; + +export const getFilterValue = (hostName: string, from: string, to: string): JsonObject => ({ + bool: { + filter: [ + { + bool: { + should: [{ match_phrase: { 'host.name': hostName } }], + minimum_should_match: 1, + }, + }, + { + bool: { + filter: [ + { + bool: { + should: [{ range: { '@timestamp': { gte: from } } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ range: { '@timestamp': { lte: to } } }], + minimum_should_match: 1, + }, + }, + ], + }, + }, + ], + }, +}); + +export const getFieldsToRequest = (): string[] => [ + '@timestamp', + 'message', + 'event.category', + 'event.action', + 'host.name', + 'source.ip', + 'destination.ip', + 'user.name', + '@timestamp', + 'signal.status', + 'signal.group.id', + 'signal.original_time', + 'signal.rule.building_block_type', + 'signal.rule.filters', + 'signal.rule.from', + 'signal.rule.language', + 'signal.rule.query', + 'signal.rule.name', + 'signal.rule.to', + 'signal.rule.id', + 'signal.rule.index', + 'signal.rule.type', + 'signal.original_event.kind', + 'signal.original_event.module', + 'file.path', + 'file.Ext.code_signature.subject_name', + 'file.Ext.code_signature.trusted', + 'file.hash.sha256', + 'host.os.family', + 'event.code', + 'kibana.rac.alert.owner', + 'kibana.rac.alert.id', + 'event.kind', +]; + +/** + * https://www.elastic.co/guide/en/elasticsearch/reference/7.12/search-fields.html#docvalue-fields + * Use the docvalue_fields parameter to get values for selected fields. + * This can be a good choice when returning a fairly small number of fields that support doc values, + * such as keywords and dates. + */ +export const getDocValueFields = (): Array<{ field: string; format?: string }> => [ + { + field: '@timestamp', + }, + { + field: 'agent.ephemeral_id', + }, + { + field: 'agent.id', + }, + { + field: 'agent.name', + }, + { + field: 'agent.type', + }, + { + field: 'agent.version', + }, + { + field: 'as.number', + }, + { + field: 'as.organization.name', + }, + { + field: 'client.address', + }, + { + field: 'client.as.number', + }, + { + field: 'client.as.organization.name', + }, + { + field: 'client.bytes', + format: 'bytes', + }, + { + field: 'client.domain', + }, + { + field: 'client.geo.city_name', + }, + { + field: 'client.geo.continent_name', + }, + { + field: 'client.geo.country_iso_code', + }, + { + field: 'client.geo.country_name', + }, + { + field: 'client.geo.location', + }, + { + field: 'client.geo.name', + }, + { + field: 'client.geo.region_iso_code', + }, + { + field: 'client.geo.region_name', + }, + { + field: 'client.ip', + }, + { + field: 'client.mac', + }, + { + field: 'client.nat.ip', + }, + { + field: 'client.nat.port', + format: 'string', + }, + { + field: 'client.packets', + }, + { + field: 'client.port', + format: 'string', + }, + { + field: 'client.registered_domain', + }, + { + field: 'client.top_level_domain', + }, + { + field: 'client.user.domain', + }, + { + field: 'client.user.email', + }, + { + field: 'client.user.full_name', + }, + { + field: 'client.user.group.domain', + }, + { + field: 'client.user.group.id', + }, + { + field: 'client.user.group.name', + }, + { + field: 'client.user.hash', + }, + { + field: 'client.user.id', + }, + { + field: 'client.user.name', + }, + { + field: 'cloud.account.id', + }, + { + field: 'cloud.availability_zone', + }, + { + field: 'cloud.instance.id', + }, + { + field: 'cloud.instance.name', + }, + { + field: 'cloud.machine.type', + }, + { + field: 'cloud.provider', + }, + { + field: 'cloud.region', + }, + { + field: 'code_signature.exists', + }, + { + field: 'code_signature.status', + }, + { + field: 'code_signature.subject_name', + }, + { + field: 'code_signature.trusted', + }, + { + field: 'code_signature.valid', + }, + { + field: 'container.id', + }, + { + field: 'container.image.name', + }, + { + field: 'container.image.tag', + }, + { + field: 'container.name', + }, + { + field: 'container.runtime', + }, + { + field: 'destination.address', + }, + { + field: 'destination.as.number', + }, + { + field: 'destination.as.organization.name', + }, + { + field: 'destination.bytes', + format: 'bytes', + }, + { + field: 'destination.domain', + }, + { + field: 'destination.geo.city_name', + }, + { + field: 'destination.geo.continent_name', + }, + { + field: 'destination.geo.country_iso_code', + }, + { + field: 'destination.geo.country_name', + }, + { + field: 'destination.geo.location', + }, + { + field: 'destination.geo.name', + }, + { + field: 'destination.geo.region_iso_code', + }, + { + field: 'destination.geo.region_name', + }, + { + field: 'destination.ip', + }, + { + field: 'destination.mac', + }, + { + field: 'destination.nat.ip', + }, + { + field: 'destination.nat.port', + format: 'string', + }, + { + field: 'destination.packets', + }, + { + field: 'destination.port', + format: 'string', + }, + { + field: 'destination.registered_domain', + }, + { + field: 'destination.top_level_domain', + }, + { + field: 'destination.user.domain', + }, + { + field: 'destination.user.email', + }, + { + field: 'destination.user.full_name', + }, + { + field: 'destination.user.group.domain', + }, + { + field: 'destination.user.group.id', + }, + { + field: 'destination.user.group.name', + }, + { + field: 'destination.user.hash', + }, + { + field: 'destination.user.id', + }, + { + field: 'destination.user.name', + }, + { + field: 'dll.code_signature.exists', + }, + { + field: 'dll.code_signature.status', + }, + { + field: 'dll.code_signature.subject_name', + }, + { + field: 'dll.code_signature.trusted', + }, + { + field: 'dll.code_signature.valid', + }, + { + field: 'dll.hash.md5', + }, + { + field: 'dll.hash.sha1', + }, + { + field: 'dll.hash.sha256', + }, + { + field: 'dll.hash.sha512', + }, + { + field: 'dll.name', + }, + { + field: 'dll.path', + }, + { + field: 'dll.pe.company', + }, + { + field: 'dll.pe.description', + }, + { + field: 'dll.pe.file_version', + }, + { + field: 'dll.pe.original_file_name', + }, + { + field: 'kibana.rac.alert.owner', + }, + { + field: 'kibana.rac.alert.id', + }, + { + field: 'event.kind', + }, +]; diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts index d42d0f5f49c5f..550148531e2ec 100644 --- a/x-pack/test/api_integration/config.ts +++ b/x-pack/test/api_integration/config.ts @@ -34,8 +34,6 @@ export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProvi '--xpack.data_enhanced.search.sessions.notTouchedTimeout=15s', // shorten notTouchedTimeout for quicker testing '--xpack.data_enhanced.search.sessions.trackingInterval=5s', // shorten trackingInterval for quicker testing '--xpack.data_enhanced.search.sessions.cleanupInterval=5s', // shorten cleanupInterval for quicker testing - '--xpack.securitySolution.enableExperimental=["ruleRegistryEnabled"]', - '--xpack.ruleRegistry.write.enabled=true', ], }, esTestCluster: {