diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md index 52579a0a14b4d..5a1b0cd265300 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md @@ -17,5 +17,6 @@ export interface ISearchStartAggsStart | | | [asScoped](./kibana-plugin-plugins-data-server.isearchstart.asscoped.md) | (request: KibanaRequest) => IScopedSearchClient | | | [getSearchStrategy](./kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md) | (name?: string) => ISearchStrategy<SearchStrategyRequest, SearchStrategyResponse> | Get other registered search strategies by name (or, by default, the Elasticsearch strategy). For example, if a new strategy needs to use the already-registered ES search strategy, it can use this function to accomplish that. | +| [searchAsInternalUser](./kibana-plugin-plugins-data-server.isearchstart.searchasinternaluser.md) | ISearchStrategy | Search as the internal Kibana system user. This is not a registered search strategy as we don't want to allow access from the client. | | [searchSource](./kibana-plugin-plugins-data-server.isearchstart.searchsource.md) | {
asScoped: (request: KibanaRequest) => Promise<ISearchStartSearchSource>;
} | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.searchasinternaluser.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.searchasinternaluser.md new file mode 100644 index 0000000000000..f6a18dafc8518 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.searchasinternaluser.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) > [searchAsInternalUser](./kibana-plugin-plugins-data-server.isearchstart.searchasinternaluser.md) + +## ISearchStart.searchAsInternalUser property + +Search as the internal Kibana system user. This is not a registered search strategy as we don't want to allow access from the client. + +Signature: + +```typescript +searchAsInternalUser: ISearchStrategy; +``` 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..f358cd78d8f90 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: 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 dc0a494aeb187..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, @@ -156,6 +157,17 @@ export class SearchService implements Plugin { ) ); + // We don't want to register this because we don't want the client to be able to access this + // strategy, but we do want to expose it to other server-side plugins + // see x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts + // for example use case + this.searchAsInternalUser = enhancedEsSearchStrategyProvider( + this.initializerContext.config.legacy.globalConfig$, + this.logger, + usage, + true + ); + this.registerSearchStrategy(EQL_SEARCH_STRATEGY, eqlSearchStrategyProvider(this.logger)); registerBsearchRoute( @@ -220,6 +232,7 @@ export class SearchService implements Plugin { uiSettings, indexPatterns, }), + searchAsInternalUser: this.searchAsInternalUser, getSearchStrategy: this.getSearchStrategy, asScoped: this.asScoped, searchSource: { diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 6192045fa04c7..26e0416b9a4b0 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -102,6 +102,11 @@ export interface ISearchStart< SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse > { aggs: AggsStart; + /** + * Search as the internal Kibana system user. This is not a registered search strategy as we don't + * want to allow access from the client. + */ + searchAsInternalUser: ISearchStrategy; /** * Get other registered search strategies by name (or, by default, the Elasticsearch strategy). * For example, if a new strategy needs to use the already-registered ES search strategy, it can diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 9826fc590a20f..47a39c99d52ef 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -1048,6 +1048,7 @@ export interface ISearchStart IScopedSearchClient; getSearchStrategy: (name?: string) => ISearchStrategy; + searchAsInternalUser: ISearchStrategy; // (undocumented) searchSource: { asScoped: (request: KibanaRequest) => Promise; @@ -1518,7 +1519,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:280:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:281:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:81:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/search/types.ts:115:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/search/types.ts:120:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts index f6564b60a8d3c..77a15eda79cef 100644 --- a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts @@ -29,6 +29,7 @@ const securityPluginStart = securityMock.createStart(); const alertingAuthorizationClientFactoryParams: jest.Mocked = { ruleTypeRegistry: ruleTypeRegistryMock.create(), getSpace: jest.fn(), + getSpaceId: jest.fn(), features, }; @@ -73,6 +74,7 @@ test('creates an alerting authorization client with proper constructor arguments features: alertingAuthorizationClientFactoryParams.features, auditLogger: expect.any(AlertingAuthorizationAuditLogger), getSpace: expect.any(Function), + getSpaceId: expect.any(Function), exemptConsumerIds: [], }); @@ -100,6 +102,7 @@ test('creates an alerting authorization client with proper constructor arguments features: alertingAuthorizationClientFactoryParams.features, auditLogger: expect.any(AlertingAuthorizationAuditLogger), getSpace: expect.any(Function), + getSpaceId: expect.any(Function), exemptConsumerIds: ['exemptConsumerA', 'exemptConsumerB'], }); @@ -122,6 +125,7 @@ test('creates an alerting authorization client with proper constructor arguments features: alertingAuthorizationClientFactoryParams.features, auditLogger: expect.any(AlertingAuthorizationAuditLogger), getSpace: expect.any(Function), + getSpaceId: expect.any(Function), exemptConsumerIds: [], }); diff --git a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts index 88a539723ca64..1df67ed8d4b79 100644 --- a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts +++ b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts @@ -19,6 +19,7 @@ export interface AlertingAuthorizationClientFactoryOpts { securityPluginSetup?: SecurityPluginSetup; securityPluginStart?: SecurityPluginStart; getSpace: (request: KibanaRequest) => Promise; + getSpaceId: (request: KibanaRequest) => string | undefined; features: FeaturesPluginStart; } @@ -29,6 +30,7 @@ export class AlertingAuthorizationClientFactory { private securityPluginSetup?: SecurityPluginSetup; private features!: FeaturesPluginStart; private getSpace!: (request: KibanaRequest) => Promise; + private getSpaceId!: (request: KibanaRequest) => string | undefined; public initialize(options: AlertingAuthorizationClientFactoryOpts) { if (this.isInitialized) { @@ -40,6 +42,7 @@ export class AlertingAuthorizationClientFactory { this.securityPluginSetup = options.securityPluginSetup; this.securityPluginStart = options.securityPluginStart; this.features = options.features; + this.getSpaceId = options.getSpaceId; } public create(request: KibanaRequest, exemptConsumerIds: string[] = []): AlertingAuthorization { @@ -48,6 +51,7 @@ export class AlertingAuthorizationClientFactory { authorization: securityPluginStart?.authz, request, getSpace: this.getSpace, + getSpaceId: this.getSpaceId, ruleTypeRegistry: this.ruleTypeRegistry, features: features!, auditLogger: new AlertingAuthorizationAuditLogger( diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts index 3ebf261b80b11..71ac9e48c7297 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts @@ -35,6 +35,7 @@ const auditLogger = alertingAuthorizationAuditLoggerMock.create(); const realAuditLogger = new AlertingAuthorizationAuditLogger(); const getSpace = jest.fn(); +const getSpaceId = () => 'space1'; const exemptConsumerIds: string[] = []; @@ -233,6 +234,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); @@ -248,6 +250,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); @@ -271,6 +274,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); @@ -297,6 +301,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); @@ -353,6 +358,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); @@ -409,6 +415,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds: ['exemptConsumer'], }); @@ -471,6 +478,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds: ['exemptConsumer'], }); @@ -539,6 +547,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); @@ -604,6 +613,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); @@ -663,6 +673,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); @@ -721,6 +732,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); @@ -783,6 +795,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); @@ -841,6 +854,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); @@ -932,6 +946,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); const { @@ -954,6 +969,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( @@ -988,6 +1004,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1050,6 +1067,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1123,6 +1141,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1197,6 +1216,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1238,6 +1258,36 @@ describe('AlertingAuthorization', () => { ] `); }); + + // This is a specific use case currently for alerts as data + // Space ids are stored in the alerts documents and even if security is disabled + // still need to consider the users space privileges + test('creates a spaceId only filter if security is disabled, but require space awareness', async () => { + const alertAuthorization = new AlertingAuthorization({ + request, + ruleTypeRegistry, + features, + auditLogger, + getSpace, + getSpaceId, + exemptConsumerIds, + }); + const { filter } = await alertAuthorization.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Alert, + { + type: AlertingAuthorizationFilterType.ESDSL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + spaceIds: 'path.to.space.id', + }, + } + ); + + expect(filter).toEqual({ + bool: { minimum_should_match: 1, should: [{ match: { 'path.to.space.id': 'space1' } }] }, + }); + }); }); describe('filterByRuleTypeAuthorization', () => { @@ -1274,6 +1324,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1355,6 +1406,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds: ['exemptConsumerA', 'exemptConsumerB'], }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1488,6 +1540,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1593,6 +1646,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds: ['exemptConsumerA'], }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1689,6 +1743,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds: ['exemptConsumerA'], }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1794,6 +1849,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1903,6 +1959,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -2009,6 +2066,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -2083,6 +2141,7 @@ describe('AlertingAuthorization', () => { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index a06bdce5a33b1..fd57298f7d93b 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -17,6 +17,7 @@ import { AlertingAuthorizationAuditLogger, ScopeType } from './audit_logger'; import { Space } from '../../../spaces/server'; import { asFiltersByRuleTypeAndConsumer, + asFiltersBySpaceId, AlertingAuthorizationFilterOpts, } from './alerting_authorization_kuery'; import { KueryNode } from '../../../../../src/plugins/data/server'; @@ -68,6 +69,7 @@ export interface ConstructorOptions { request: KibanaRequest; features: FeaturesPluginStart; getSpace: (request: KibanaRequest) => Promise; + getSpaceId: (request: KibanaRequest) => string | undefined; auditLogger: AlertingAuthorizationAuditLogger; exemptConsumerIds: string[]; authorization?: SecurityPluginSetup['authz']; @@ -81,7 +83,7 @@ export class AlertingAuthorization { private readonly featuresIds: Promise>; private readonly allPossibleConsumers: Promise; private readonly exemptConsumerIds: string[]; - private readonly spaceId: Promise; + private readonly spaceId: string | undefined; constructor({ ruleTypeRegistry, @@ -90,6 +92,7 @@ export class AlertingAuthorization { features, auditLogger, getSpace, + getSpaceId, exemptConsumerIds, }: ConstructorOptions) { this.request = request; @@ -102,7 +105,7 @@ export class AlertingAuthorization { // manually authorize each rule type in the management UI. this.exemptConsumerIds = exemptConsumerIds; - this.spaceId = getSpace(request).then((maybeSpace) => maybeSpace?.id); + this.spaceId = getSpaceId(request); this.featuresIds = getSpace(request) .then((maybeSpace) => new Set(maybeSpace?.disabledFeatures ?? [])) @@ -141,7 +144,7 @@ export class AlertingAuthorization { return this.authorization?.mode?.useRbacForRequest(this.request) ?? false; } - public async getSpaceId(): Promise { + public getSpaceId(): string | undefined { return this.spaceId; } @@ -303,7 +306,7 @@ export class AlertingAuthorization { const authorizedEntries: Map> = new Map(); return { - filter: asFiltersByRuleTypeAndConsumer(authorizedRuleTypes, filterOpts), + filter: asFiltersByRuleTypeAndConsumer(authorizedRuleTypes, filterOpts, this.spaceId), ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, authType: string) => { if (!authorizedRuleTypeIdsToConsumers.has(`${ruleTypeId}/${consumer}/${authType}`)) { throw Boom.forbidden( @@ -345,7 +348,9 @@ export class AlertingAuthorization { }, }; } + return { + filter: asFiltersBySpaceId(filterOpts, this.spaceId), ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, authType: string) => {}, logSuccessfulAuthorization: () => {}, }; diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts index 7d39380f7bd1a..5ea15c4818a21 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts @@ -10,6 +10,7 @@ import { AlertingAuthorizationFilterType, asFiltersByRuleTypeAndConsumer, ensureFieldIsSafeForQuery, + asFiltersBySpaceId, } from './alerting_authorization_kuery'; import { esKuery } from '../../../../../src/plugins/data/server'; @@ -39,7 +40,8 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { ruleTypeId: 'path.to.rule.id', consumer: 'consumer-field', }, - } + }, + 'space1' ) ).toEqual( esKuery.fromKueryExpression(`((path.to.rule.id:myAppAlertType and consumer-field:(myApp)))`) @@ -73,7 +75,8 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { ruleTypeId: 'path.to.rule.id', consumer: 'consumer-field', }, - } + }, + 'space1' ) ).toEqual( esKuery.fromKueryExpression( @@ -144,7 +147,8 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { ruleTypeId: 'path.to.rule.id', consumer: 'consumer-field', }, - } + }, + 'space1' ) ).toEqual( esKuery.fromKueryExpression( @@ -152,6 +156,118 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { ) ); }); + + test('constructs KQL filter with spaceId filter when spaceIds field path exists', async () => { + expect( + asFiltersByRuleTypeAndConsumer( + new Set([ + { + actionGroups: [], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + recoveryActionGroup: RecoveredActionGroup, + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + authorizedConsumers: { + alerts: { read: true, all: true }, + myApp: { read: true, all: true }, + myOtherApp: { read: true, all: true }, + myAppWithSubFeature: { read: true, all: true }, + }, + enabledInLicense: true, + }, + { + actionGroups: [], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + recoveryActionGroup: RecoveredActionGroup, + id: 'myOtherAppAlertType', + name: 'myOtherAppAlertType', + producer: 'alerts', + authorizedConsumers: { + alerts: { read: true, all: true }, + myApp: { read: true, all: true }, + myOtherApp: { read: true, all: true }, + myAppWithSubFeature: { read: true, all: true }, + }, + enabledInLicense: true, + }, + ]), + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + spaceIds: 'path.to.spaceIds', + }, + }, + 'space1' + ) + ).toEqual( + esKuery.fromKueryExpression( + `((path.to.rule.id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature) and path.to.spaceIds:space1) or (path.to.rule.id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature) and path.to.spaceIds:space1))` + ) + ); + }); + + test('constructs KQL filter without spaceId filter when spaceIds path is specified, but spaceId is undefined', async () => { + expect( + asFiltersByRuleTypeAndConsumer( + new Set([ + { + actionGroups: [], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + recoveryActionGroup: RecoveredActionGroup, + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + authorizedConsumers: { + alerts: { read: true, all: true }, + myApp: { read: true, all: true }, + myOtherApp: { read: true, all: true }, + myAppWithSubFeature: { read: true, all: true }, + }, + enabledInLicense: true, + }, + { + actionGroups: [], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + recoveryActionGroup: RecoveredActionGroup, + id: 'myOtherAppAlertType', + name: 'myOtherAppAlertType', + producer: 'alerts', + authorizedConsumers: { + alerts: { read: true, all: true }, + myApp: { read: true, all: true }, + myOtherApp: { read: true, all: true }, + myAppWithSubFeature: { read: true, all: true }, + }, + enabledInLicense: true, + }, + ]), + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + spaceIds: 'path.to.spaceIds', + }, + }, + undefined + ) + ).toEqual( + esKuery.fromKueryExpression( + `((path.to.rule.id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` + ) + ); + }); }); describe('asEsDslFiltersByRuleTypeAndConsumer', () => { @@ -180,7 +296,8 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { ruleTypeId: 'path.to.rule.id', consumer: 'consumer-field', }, - } + }, + 'space1' ) ).toEqual({ bool: { @@ -241,7 +358,8 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { ruleTypeId: 'path.to.rule.id', consumer: 'consumer-field', }, - } + }, + 'space1' ) ).toEqual({ bool: { @@ -344,7 +462,8 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { ruleTypeId: 'path.to.rule.id', consumer: 'consumer-field', }, - } + }, + 'space1' ) ).toEqual({ bool: { @@ -485,6 +604,73 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { }); }); +describe('asFiltersBySpaceId', () => { + test('returns ES dsl filter of spaceId', () => { + expect( + asFiltersBySpaceId( + { + type: AlertingAuthorizationFilterType.ESDSL, + fieldNames: { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + spaceIds: 'path.to.space.id', + }, + }, + 'space1' + ) + ).toEqual({ + bool: { minimum_should_match: 1, should: [{ match: { 'path.to.space.id': 'space1' } }] }, + }); + }); + + test('returns KQL filter of spaceId', () => { + expect( + asFiltersBySpaceId( + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + spaceIds: 'path.to.space.id', + }, + }, + 'space1' + ) + ).toEqual(esKuery.fromKueryExpression('(path.to.space.id: space1)')); + }); + + test('returns undefined if no path to spaceIds is provided', () => { + expect( + asFiltersBySpaceId( + { + type: AlertingAuthorizationFilterType.ESDSL, + fieldNames: { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + }, + }, + 'space1' + ) + ).toBeUndefined(); + }); + + test('returns undefined if spaceId is undefined', () => { + expect( + asFiltersBySpaceId( + { + type: AlertingAuthorizationFilterType.ESDSL, + fieldNames: { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + spaceIds: 'path.to.space.id', + }, + }, + undefined + ) + ).toBeUndefined(); + }); +}); + describe('ensureFieldIsSafeForQuery', () => { test('throws if field contains character that isnt safe in a KQL query', () => { expect(() => ensureFieldIsSafeForQuery('id', 'alert-*')).toThrowError( 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 efba94958fcbf..32b3bcc866577 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts @@ -7,8 +7,8 @@ import { remove } from 'lodash'; import { JsonObject } from '@kbn/common-utils'; -import { EsQueryConfig, nodeBuilder, toElasticsearchQuery } from '@kbn/es-query'; -import { KueryNode } from '../../../../../src/plugins/data/server'; +import { EsQueryConfig, nodeBuilder, toElasticsearchQuery, KueryNode } from '@kbn/es-query'; + import { RegistryAlertTypeWithAuth } from './alerting_authorization'; export enum AlertingAuthorizationFilterType { @@ -24,6 +24,7 @@ export interface AlertingAuthorizationFilterOpts { interface AlertingAuthorizationFilterFieldNames { ruleTypeId: string; consumer: string; + spaceIds?: string; } const esQueryConfig: EsQueryConfig = { @@ -35,22 +36,28 @@ const esQueryConfig: EsQueryConfig = { export function asFiltersByRuleTypeAndConsumer( ruleTypes: Set, - opts: AlertingAuthorizationFilterOpts + opts: AlertingAuthorizationFilterOpts, + spaceId: string | undefined ): KueryNode | JsonObject { const kueryNode = nodeBuilder.or( Array.from(ruleTypes).reduce((filters, { id, authorizedConsumers }) => { ensureFieldIsSafeForQuery('ruleTypeId', id); - filters.push( - nodeBuilder.and([ - nodeBuilder.is(opts.fieldNames.ruleTypeId, id), - nodeBuilder.or( - Object.keys(authorizedConsumers).map((consumer) => { - ensureFieldIsSafeForQuery('consumer', consumer); - return nodeBuilder.is(opts.fieldNames.consumer, consumer); - }) - ), - ]) - ); + const andNodes = [ + nodeBuilder.is(opts.fieldNames.ruleTypeId, id), + nodeBuilder.or( + Object.keys(authorizedConsumers).map((consumer) => { + ensureFieldIsSafeForQuery('consumer', consumer); + return nodeBuilder.is(opts.fieldNames.consumer, consumer); + }) + ), + ]; + + if (opts.fieldNames.spaceIds != null && spaceId != null) { + andNodes.push(nodeBuilder.is(opts.fieldNames.spaceIds, spaceId)); + } + + filters.push(nodeBuilder.and(andNodes)); + return filters; }, []) ); @@ -62,6 +69,29 @@ export function asFiltersByRuleTypeAndConsumer( return kueryNode; } +// This is a specific use case currently for alerts as data +// Space ids are stored in the alerts documents and even if security is disabled +// still need to consider the users space privileges +export function asFiltersBySpaceId( + opts: AlertingAuthorizationFilterOpts, + spaceId: string | undefined +): KueryNode | JsonObject | undefined { + if (opts.fieldNames.spaceIds != null && spaceId != null) { + const kueryNode = nodeBuilder.is(opts.fieldNames.spaceIds, spaceId); + + switch (opts.type) { + case AlertingAuthorizationFilterType.ESDSL: + return toElasticsearchQuery(kueryNode, undefined, esQueryConfig); + case AlertingAuthorizationFilterType.KQL: + return kueryNode; + default: + return undefined; + } + } + + return undefined; +} + export function ensureFieldIsSafeForQuery(field: string, value: string): boolean { const invalid = value.match(/([>=<\*:()]+|\s+)/g); if (invalid) { diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 0b5fb54f17573..197cf49edc147 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -335,6 +335,9 @@ export class AlertingPlugin { async getSpace(request: KibanaRequest) { return plugins.spaces?.spacesService.getActiveSpace(request); }, + getSpaceId(request: KibanaRequest) { + return plugins.spaces?.spacesService.getSpaceId(request); + }, features: plugins.features, }); 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..598818a7a69c3 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 & @@ -63,13 +67,15 @@ export class AlertsClient { private readonly auditLogger?: AuditLogger; private readonly authorization: PublicMethodsOf; private readonly esClient: ElasticsearchClient; - private readonly spaceId: Promise; + private readonly spaceId: string | undefined; constructor({ auditLogger, authorization, logger, esClient }: ConstructorOptions) { this.logger = logger; this.authorization = authorization; this.esClient = esClient; this.auditLogger = auditLogger; + // If spaceId is undefined, it means that spaces is disabled + // Otherwise, if space is enabled and not specified, it is "default" this.spaceId = this.authorization.getSpaceId(); } @@ -89,7 +95,7 @@ export class AlertsClient { index, }: GetAlertParams): Promise<(AlertType & { _version: string | undefined }) | null | undefined> { try { - const alertSpaceId = await this.spaceId; + const alertSpaceId = this.spaceId; if (alertSpaceId == null) { this.logger.error('Failed to acquire spaceId from authorization client'); return; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts index bf5375c55c04b..518b75637cf34 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts @@ -27,7 +27,7 @@ const alertsClientParams: jest.Mocked = { beforeEach(() => { jest.resetAllMocks(); - alertingAuthMock.getSpaceId.mockImplementation(() => Promise.resolve('test_default_space_id')); + alertingAuthMock.getSpaceId.mockImplementation(() => 'test_default_space_id'); }); describe('get()', () => { diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts index fd3e0f00a097b..e24bae96975df 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts @@ -27,7 +27,7 @@ const alertsClientParams: jest.Mocked = { beforeEach(() => { jest.resetAllMocks(); - alertingAuthMock.getSpaceId.mockImplementation(() => Promise.resolve('test_default_space_id')); + alertingAuthMock.getSpaceId.mockImplementation(() => 'test_default_space_id'); }); describe('update()', () => { 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/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 48a23a967059e..4e2fca63bc51d 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -25,7 +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_ALERTS_INDEX = '.alerts-security.alerts'; export const DEFAULT_SIGNALS_INDEX = '.siem-signals'; export const DEFAULT_LISTS_INDEX = '.lists'; export const DEFAULT_ITEMS_INDEX = '.items'; 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 index d4ce280e73268..40c1246733056 100644 --- 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 @@ -74,7 +74,6 @@ export const createQueryAlertType = (ruleDataClient: RuleDataClient, logger: Log }; const alerts = await findAlerts(query); - // console.log('alerts', alerts); alertWithPersistence(alerts).forEach((alert) => { alert.scheduleActions('default', { server: 'server-test' }); }); 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 index c34af7dee4044..b1d614e98ccae 100755 --- 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 @@ -6,7 +6,7 @@ # 2.0. # -curl -X POST http://localhost:5601/${BASE_PATH}/api/alerts/alert \ +curl -X POST ${KIBANA_URL}${SPACE_URL}/api/alerts/alert \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -H 'kbn-xsrf: true' \ -H 'Content-Type: application/json' \ diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 4a346581b7767..2726afdf31ee0 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 { @@ -66,6 +68,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'; @@ -211,7 +214,7 @@ export class Plugin implements IPlugin { indexName: string; eventId: string; + authFilter?: JsonObject; } diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/index.ts index c4d6f70a27587..82e439b386d60 100644 --- a/x-pack/plugins/timelines/common/search_strategy/timeline/events/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/index.ts @@ -16,3 +16,8 @@ export enum TimelineEventsQueries { kpi = 'eventsKpi', lastEventTime = 'eventsLastEventTime', } + +export enum EntityType { + ALERTS = 'alerts', + EVENTS = 'events', +} diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts index 7064ef033fc5a..cb5e27ec84d47 100644 --- a/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { ALERTS_CONSUMERS } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; import { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; import { ESQuery } from '../../typed_json'; @@ -16,6 +17,7 @@ import { TimelineEventsLastEventTimeRequestOptions, TimelineEventsLastEventTimeStrategyResponse, TimelineKpiStrategyResponse, + EntityType, } from './events'; import { DocValueFields, @@ -41,6 +43,8 @@ export interface TimelineRequestBasicOptions extends IEsSearchRequest { defaultIndex: string[]; docValueFields?: DocValueFields[]; factoryQueryType?: TimelineFactoryQueryTypes; + entityType?: EntityType; + alertConsumers?: ALERTS_CONSUMERS[]; } export interface TimelineRequestSortField 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 b40dc728741ed..4ca8edf9d4539 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..ba9aa845f4b9b 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/factory/events/details/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/index.ts index 26e6267b36d77..c82d9af938a98 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/index.ts @@ -26,9 +26,9 @@ import { } from '../../../../../../common/utils/field_formatters'; export const timelineEventsDetails: TimelineFactory = { - buildDsl: (options: TimelineEventsDetailsRequestOptions) => { + buildDsl: ({ authFilter, ...options }: TimelineEventsDetailsRequestOptions) => { const { indexName, eventId, docValueFields = [] } = options; - return buildTimelineDetailsQuery(indexName, eventId, docValueFields); + return buildTimelineDetailsQuery(indexName, eventId, docValueFields, authFilter); }, parse: async ( options: TimelineEventsDetailsRequestOptions, diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts index 757b004d23f42..0517dcfb64901 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts @@ -5,25 +5,43 @@ * 2.0. */ +import { JsonObject } from '@kbn/common-utils'; import { DocValueFields } from '../../../../../../common/search_strategy'; export const buildTimelineDetailsQuery = ( indexName: string, id: string, - docValueFields: DocValueFields[] -) => ({ - allowNoIndices: true, - index: indexName, - ignoreUnavailable: true, - body: { - docvalue_fields: docValueFields, - query: { - terms: { - _id: [id], - }, + docValueFields: DocValueFields[], + authFilter?: JsonObject +) => { + const basicFilter = { + terms: { + _id: [id], }, - fields: [{ field: '*', include_unmapped: true }], - _source: true, - }, - size: 1, -}); + }; + const query = + authFilter != null + ? { + bool: { + filter: [basicFilter, authFilter], + }, + } + : { + terms: { + _id: [id], + }, + }; + + return { + allowNoIndices: true, + index: indexName, + ignoreUnavailable: true, + body: { + docvalue_fields: docValueFields, + query, + fields: [{ field: '*', include_unmapped: true }], + _source: true, + }, + size: 1, + }; +}; diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts index 0fc6ce78ee982..354c682377bac 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts @@ -6,6 +6,7 @@ */ import { isEmpty } from 'lodash/fp'; +import { ISearchRequestParams } from 'src/plugins/data/common'; import { TimelineEventsLastEventTimeRequestOptions, LastEventIndexKey, @@ -106,5 +107,7 @@ export const buildLastEventTimeQuery = ({ return assertUnreachable(eventIndexKey); } }; - return getQuery(indexKey); + // TODO: Yes, TypeScript defeated me. Need to remove this type + // cast, typing issue seemed to have slipped into codebase previously + return getQuery(indexKey) as ISearchRequestParams; }; diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/types.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/types.ts index 2f0f279c5baa0..831e3266ad8ee 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/types.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/types.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; +import { + IEsSearchResponse, + ISearchRequestParams, +} from '../../../../../../../src/plugins/data/common'; import { TimelineFactoryQueryTypes, TimelineStrategyRequestType, @@ -13,7 +16,7 @@ import { } from '../../../../common/search_strategy/timeline'; export interface TimelineFactory { - buildDsl: (options: TimelineStrategyRequestType) => unknown; + buildDsl: (options: TimelineStrategyRequestType) => ISearchRequestParams; parse: ( options: TimelineStrategyRequestType, response: IEsSearchResponse 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..e9d66d2643a6e 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts @@ -5,43 +5,77 @@ * 2.0. */ -import { map, mergeMap } from 'rxjs/operators'; +import { OWNER, RULE_ID, SPACE_IDS } from '@kbn/rule-data-utils/target/technical_field_names'; +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 AlertingPluginStartContract, +} 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: AlertingPluginStartContract ): 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 (alertConsumers != null && entityType != null && entityType === EntityType.ALERTS) { + const allFeatureIdsValid = alertConsumers.every((id) => isValidFeatureId(id)); + + if (!allFeatureIdsValid) { + throw new Error('An invalid alerts consumer feature id was provided'); + } + + 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) { @@ -50,3 +84,82 @@ export const timelineSearchStrategyProvider = ({ + es, + request, + options, + deps, + queryFactory, +}: { + es: ISearchStrategy; + request: TimelineStrategyRequestType; + options: ISearchOptions; + deps: SearchStrategyDependencies; + queryFactory: TimelineFactory; +}) => { + 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 timelineAlertsSearchStrategy = ({ + es, + request, + options, + deps, + queryFactory, + alerting, + alertConsumers, +}: { + es: ISearchStrategy; + request: TimelineStrategyRequestType; + options: ISearchOptions; + deps: SearchStrategyDependencies; + alerting: AlertingPluginStartContract; + queryFactory: TimelineFactory; + alertConsumers: ALERTS_CONSUMERS[]; +}) => { + // Based on what solution alerts you want to see, figures out what corresponding + // index to query (ex: siem --> .alerts-security.alerts) + const indices = alertConsumers.flatMap((consumer) => mapConsumerToIndexName[consumer]); + const requestWithAlertsIndices = { ...request, defaultIndex: indices, indexName: 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, + // Not passing in values, these are the paths for these fields + fieldNames: { + consumer: OWNER, + ruleTypeId: RULE_ID, + spaceIds: 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..26748c37fa1e1 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 AlertingPluginStartContract } 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: AlertingPluginStartContract; } 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..c99e41e011a9a 100644 --- a/x-pack/test/api_integration/apis/security_solution/events.ts +++ b/x-pack/test/api_integration/apis/security_solution/events.ts @@ -6,21 +6,18 @@ */ import expect from '@kbn/expect'; +import { JsonObject } from '@kbn/common-utils'; -import { secOnly } from '../../../rule_registry/common/lib/authentication/users'; -import { - createSpacesAndUsers, - deleteSpacesAndUsers, -} from '../../../rule_registry/common/lib/authentication/'; import { Direction, TimelineEventsQueries, } from '../../../../plugins/security_solution/common/search_strategy'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { getDocValueFields, getFieldsToRequest, getFilterValue } from './utils'; const TO = '3000-01-01T00:00:00.000Z'; const FROM = '2000-01-01T00:00:00.000Z'; - +const TEST_URL = '/internal/search/timelineSearchStrategy/'; // typical values that have to change after an update from "scripts/es_archiver" const DATA_COUNT = 7; const HOST_NAME = 'suricata-sensor-amsterdam'; @@ -30,538 +27,80 @@ 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, - }, - }, - ], - }, - }, - ], - }, -}; - -/** - * 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: 'events', + 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 () => { await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); - await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); - await createSpacesAndUsers(getService); }); after(async () => { await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); - await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); - 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); + 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); - }); + 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', - }, - ], - 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); - - const timeline = resp.body; - - // 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); - }); - }); - - 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, - }, - language: 'kuery', - sort: [ - { - field: '@timestamp', - direction: Direction.desc, - type: 'number', - }, - ], - timerange: { - from: FROM, - to: TO, - interval: '12h', - }, - }) - .expect(200); + }) + .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]); - }); + 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]); }); }); } 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..9265a0066d208 --- /dev/null +++ b/x-pack/test/api_integration/apis/security_solution/utils.ts @@ -0,0 +1,386 @@ +/* + * 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, JsonArray } 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', +]; + +/** + * 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 = (): JsonArray => [ + { + 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', + }, +]; diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts index 550148531e2ec..9721a1827caf3 100644 --- a/x-pack/test/api_integration/config.ts +++ b/x-pack/test/api_integration/config.ts @@ -34,6 +34,7 @@ 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.ruleRegistry.write.enabled=true', ], }, esTestCluster: { diff --git a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json index 36a73c1994c99..6d7f78292ddac 100644 --- a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json +++ b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json @@ -4,6 +4,7 @@ "index": ".alerts-observability-apm", "id": "NoxgpHkBqbdrfX07MqXV", "source": { + "event.kind" : "signal", "@timestamp": "2020-12-16T15:16:18.570Z", "rule.id": "apm.error_rate", "message": "hello world 1", @@ -20,6 +21,7 @@ "index": ".alerts-observability-apm", "id": "space1alert", "source": { + "event.kind" : "signal", "@timestamp": "2020-12-16T15:16:18.570Z", "rule.id": "apm.error_rate", "message": "hello world 1", @@ -36,6 +38,7 @@ "index": ".alerts-observability-apm", "id": "space2alert", "source": { + "event.kind" : "signal", "@timestamp": "2020-12-16T15:16:18.570Z", "rule.id": "apm.error_rate", "message": "hello world 1", @@ -52,6 +55,7 @@ "index": ".alerts-security.alerts", "id": "020202", "source": { + "event.kind" : "signal", "@timestamp": "2020-12-16T15:16:18.570Z", "rule.id": "siem.signals", "message": "hello world security", @@ -61,3 +65,20 @@ } } } + +{ + "type": "doc", + "value": { + "index": ".alerts-security.alerts", + "id": "020204", + "source": { + "event.kind" : "signal", + "@timestamp": "2020-12-16T15:16:18.570Z", + "rule.id": "siem.customRule", + "message": "hello world security", + "kibana.rac.alert.owner": "siem", + "kibana.rac.alert.status": "open", + "kibana.space_ids": ["space1", "space2"] + } + } +} diff --git a/x-pack/test/rule_registry/common/lib/authentication/spaces.ts b/x-pack/test/rule_registry/common/lib/authentication/spaces.ts index 556b1686601ff..a4b6037c6d9de 100644 --- a/x-pack/test/rule_registry/common/lib/authentication/spaces.ts +++ b/x-pack/test/rule_registry/common/lib/authentication/spaces.ts @@ -19,7 +19,13 @@ const space2: Space = { disabledFeatures: [], }; -export const spaces: Space[] = [space1, space2]; +const other: Space = { + id: 'other', + name: 'Other Space', + disabledFeatures: [], +}; + +export const spaces: Space[] = [space1, space2, other]; export const getSpaceUrlPrefix = (spaceId?: string) => { return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/get_alert_by_id.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/get_alert_by_id.ts index df188718bff19..2543f8d73ff05 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/get_alert_by_id.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/get_alert_by_id.ts @@ -51,7 +51,7 @@ export default ({ getService }: FtrProviderContext) => { expect(securitySolution).to.eql(SECURITY_SOLUTION_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below }; - describe('Alerts - GET - RBAC - spaces', () => { + describe('Alerts - GET - RBAC', () => { before(async () => { await getSecuritySolutionIndexName(superUser); await getAPMIndexName(superUser); diff --git a/x-pack/test/timeline/common/config.ts b/x-pack/test/timeline/common/config.ts new file mode 100644 index 0000000000000..ba1c8528527e4 --- /dev/null +++ b/x-pack/test/timeline/common/config.ts @@ -0,0 +1,97 @@ +/* + * 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 { CA_CERT_PATH } from '@kbn/dev-utils'; +import { FtrConfigProviderContext } from '@kbn/test'; + +import { services } from './services'; +import { getAllExternalServiceSimulatorPaths } from '../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +interface CreateTestConfigOptions { + license: string; + disabledPlugins?: string[]; + ssl?: boolean; + testFiles?: string[]; +} + +// test.not-enabled is specifically not enabled +const enabledActionTypes = [ + '.email', + '.index', + '.jira', + '.pagerduty', + '.resilient', + '.server-log', + '.servicenow', + '.servicenow-sir', + '.slack', + '.webhook', + '.case', + 'test.authorization', + 'test.failing', + 'test.index-record', + 'test.noop', + 'test.rate-limit', +]; + +export function createTestConfig(name: string, options: CreateTestConfigOptions) { + const { license = 'trial', disabledPlugins = [], ssl = false, testFiles = [] } = options; + + return async ({ readConfigFile }: FtrConfigProviderContext) => { + const xPackApiIntegrationTestsConfig = await readConfigFile( + require.resolve('../../api_integration/config.ts') + ); + + const servers = { + ...xPackApiIntegrationTestsConfig.get('servers'), + elasticsearch: { + ...xPackApiIntegrationTestsConfig.get('servers.elasticsearch'), + protocol: ssl ? 'https' : 'http', + }, + }; + + return { + testFiles: testFiles ? testFiles : [require.resolve('../tests/common')], + servers, + services, + junit: { + reportName: 'X-Pack Timeline plugin API Integration Tests', + }, + esTestCluster: { + ...xPackApiIntegrationTestsConfig.get('esTestCluster'), + license, + ssl, + serverArgs: [ + `xpack.license.self_generated.type=${license}`, + `xpack.security.enabled=${ + !disabledPlugins.includes('security') && ['trial', 'basic'].includes(license) + }`, + ], + }, + kbnTestServer: { + ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), + `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, + `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + '--xpack.eventLog.logEntries=true', + ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), + // TO DO: Remove feature flags once we're good to go + '--xpack.securitySolution.enableExperimental=["ruleRegistryEnabled"]', + '--xpack.ruleRegistry.write.enabled=true', + `--server.xsrf.whitelist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, + ...(ssl + ? [ + `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + ] + : []), + ], + }, + }; + }; +} diff --git a/x-pack/test/timeline/common/ftr_provider_context.d.ts b/x-pack/test/timeline/common/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..aa56557c09df8 --- /dev/null +++ b/x-pack/test/timeline/common/ftr_provider_context.d.ts @@ -0,0 +1,12 @@ +/* + * 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 { GenericFtrProviderContext } from '@kbn/test'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/timeline/common/services.ts b/x-pack/test/timeline/common/services.ts new file mode 100644 index 0000000000000..7e415338c405f --- /dev/null +++ b/x-pack/test/timeline/common/services.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 { services } from '../../api_integration/services'; diff --git a/x-pack/test/timeline/security_and_spaces/config_basic.ts b/x-pack/test/timeline/security_and_spaces/config_basic.ts new file mode 100644 index 0000000000000..98b7b1abe98e7 --- /dev/null +++ b/x-pack/test/timeline/security_and_spaces/config_basic.ts @@ -0,0 +1,15 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + license: 'basic', + ssl: true, + testFiles: [require.resolve('./tests/basic')], +}); diff --git a/x-pack/test/timeline/security_and_spaces/config_trial.ts b/x-pack/test/timeline/security_and_spaces/config_trial.ts new file mode 100644 index 0000000000000..b5328fd83c2cb --- /dev/null +++ b/x-pack/test/timeline/security_and_spaces/config_trial.ts @@ -0,0 +1,15 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + license: 'trial', + ssl: true, + testFiles: [require.resolve('./tests/trial')], +}); diff --git a/x-pack/test/timeline/security_and_spaces/tests/basic/events.ts b/x-pack/test/timeline/security_and_spaces/tests/basic/events.ts new file mode 100644 index 0000000000000..f4b7efc096e33 --- /dev/null +++ b/x-pack/test/timeline/security_and_spaces/tests/basic/events.ts @@ -0,0 +1,408 @@ +/* + * 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 { JsonObject } from '@kbn/common-utils'; + +import { User } from '../../../../rule_registry/common/lib/authentication/types'; +import { TimelineEdges, TimelineNonEcsData } from '../../../../../plugins/timelines/common/'; +import { getSpaceUrlPrefix } from '../../../../rule_registry/common/lib/authentication/spaces'; + +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 { + Direction, + TimelineEventsQueries, +} from '../../../../../plugins/security_solution/common/search_strategy'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +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'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const getPostBody = (): JsonObject => ({ + defaultIndex: ['.alerts-*'], + entityType: 'alerts', + docValueFields: [ + { + field: '@timestamp', + }, + { + field: 'kibana.rac.alert.owner', + }, + { + field: 'kibana.rac.alert.id', + }, + { + field: 'event.kind', + }, + ], + factoryQueryType: TimelineEventsQueries.all, + fieldRequested: [ + '@timestamp', + 'message', + 'kibana.rac.alert.owner', + 'kibana.rac.alert.id', + 'event.kind', + ], + fields: [], + filterQuery: { + bool: { + filter: [ + { + match_all: {}, + }, + ], + }, + }, + pagination: { + activePage: 0, + querySize: 25, + }, + language: 'kuery', + sort: [ + { + field: '@timestamp', + direction: Direction.desc, + type: 'number', + }, + ], + timerange: { + from: FROM, + to: TO, + interval: '12h', + }, + }); + + describe('Timeline - Events', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + 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: TimelineEdges) => { + const data: TimelineNonEcsData[] = hit.node.data; + return data.some(({ field, value }) => { + return ( + field === 'kibana.rac.alert.owner' && + featureIds.includes((value && 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); + }); + }); + } + + 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 }) + // TODO - This should be updated to be a 403 once this ticket is resolved + // https://github.com/elastic/kibana/issues/106005 + .expect(500); + }); + }); + } + + 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: 2, + body: { + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: 'alerts', + alertConsumers: ['siem'], + }, + authorizedUsers: [ + ...authorizedSecSpace1, + ...authorizedSecObsSpace1, + ...authorizedSecInAllSpaces, + ...authorizedSecObsInAllSpaces, + ...authorizedInAllSpaces, + ], + usersWithoutAllPrivileges: [...authorizedObsSpace1, ...authorizedObsInAllSpaces], + unauthorizedUsers: [ + ...authorizedSecSpace2, + ...authorizedObsSpace2, + ...authorizedSecObsSpace2, + ...unauthorized, + ], + }); + + addTests({ + space: SPACE_2, + featureIds: ['siem'], + expectedNumberAlerts: 2, + body: { + ...getPostBody(), + alertConsumers: ['siem'], + }, + authorizedUsers: [ + ...authorizedSecSpace2, + ...authorizedSecObsSpace2, + ...authorizedSecInAllSpaces, + ...authorizedSecObsInAllSpaces, + ...authorizedInAllSpaces, + ], + usersWithoutAllPrivileges: [...authorizedObsSpace2, ...authorizedObsInAllSpaces], + unauthorizedUsers: [ + ...authorizedSecSpace1, + ...authorizedObsSpace1, + ...authorizedSecObsSpace1, + ...unauthorized, + ], + }); + }); + + describe('Querying for APM alerts only', () => { + addTests({ + space: SPACE_1, + featureIds: ['apm'], + expectedNumberAlerts: 2, + body: { + ...getPostBody(), + alertConsumers: ['apm'], + }, + authorizedUsers: [ + ...authorizedObsSpace1, + ...authorizedSecObsSpace1, + ...authorizedObsInAllSpaces, + ...authorizedSecObsInAllSpaces, + ...authorizedInAllSpaces, + ], + usersWithoutAllPrivileges: [...authorizedSecSpace1, ...authorizedSecInAllSpaces], + unauthorizedUsers: [ + ...authorizedSecSpace2, + ...authorizedObsSpace2, + ...authorizedSecObsSpace2, + ...unauthorized, + ], + }); + addTests({ + space: SPACE_2, + featureIds: ['apm'], + expectedNumberAlerts: 2, + body: { + ...getPostBody(), + alertConsumers: ['apm'], + }, + authorizedUsers: [ + ...authorizedObsSpace2, + ...authorizedSecObsSpace2, + ...authorizedObsInAllSpaces, + ...authorizedSecObsInAllSpaces, + ...authorizedInAllSpaces, + ], + usersWithoutAllPrivileges: [...authorizedSecSpace2, ...authorizedSecInAllSpaces], + unauthorizedUsers: [ + ...authorizedSecSpace1, + ...authorizedObsSpace1, + ...authorizedSecObsSpace1, + ...unauthorized, + ], + }); + }); + + describe('Querying for multiple solutions', () => { + describe('authorized for both security solution and apm', () => { + addTests({ + space: SPACE_1, + featureIds: ['siem', 'apm'], + expectedNumberAlerts: 4, + body: { + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: 'alerts', + alertConsumers: ['siem', 'apm'], + }, + authorizedUsers: [ + ...authorizedSecObsSpace1, + ...authorizedSecObsInAllSpaces, + ...authorizedInAllSpaces, + ], + unauthorizedUsers: [...unauthorized], + }); + addTests({ + space: SPACE_2, + featureIds: ['siem', 'apm'], + expectedNumberAlerts: 4, + body: { + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: 'alerts', + alertConsumers: ['siem', 'apm'], + }, + authorizedUsers: [ + ...authorizedSecObsSpace2, + ...authorizedSecObsInAllSpaces, + ...authorizedInAllSpaces, + ], + unauthorizedUsers: [...unauthorized], + }); + }); + describe('security solution privileges only', () => { + addTests({ + space: SPACE_1, + featureIds: ['siem'], + expectedNumberAlerts: 2, + body: { + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: 'alerts', + alertConsumers: ['siem', 'apm'], + }, + authorizedUsers: [...authorizedSecInAllSpaces], + unauthorizedUsers: [...unauthorized], + }); + }); + + describe('apm privileges only', () => { + addTests({ + space: SPACE_1, + featureIds: ['apm'], + expectedNumberAlerts: 2, + body: { + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: 'alerts', + alertConsumers: ['siem', 'apm'], + }, + authorizedUsers: [...authorizedObsInAllSpaces], + unauthorizedUsers: [...unauthorized], + }); + }); + + describe('querying from default space when no alerts were created in default space', () => { + addTests({ + featureIds: ['siem'], + expectedNumberAlerts: 0, + body: { + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: 'alerts', + alertConsumers: ['siem', 'apm'], + }, + authorizedUsers: [...authorizedSecInAllSpaces], + unauthorizedUsers: [...unauthorized], + }); + }); + }); + }); + }); +}; diff --git a/x-pack/test/timeline/security_and_spaces/tests/basic/index.ts b/x-pack/test/timeline/security_and_spaces/tests/basic/index.ts new file mode 100644 index 0000000000000..4672a8e2e7f65 --- /dev/null +++ b/x-pack/test/timeline/security_and_spaces/tests/basic/index.ts @@ -0,0 +1,31 @@ +/* + * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createSpacesAndUsers, + deleteSpacesAndUsers, +} from '../../../../rule_registry/common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile, getService }: FtrProviderContext): void => { + describe('timeline security and spaces enabled: basic', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + before(async () => { + await createSpacesAndUsers(getService); + }); + + after(async () => { + await deleteSpacesAndUsers(getService); + }); + + // Basic + loadTestFile(require.resolve('./events')); + }); +}; diff --git a/x-pack/test/timeline/security_and_spaces/tests/trial/events.ts b/x-pack/test/timeline/security_and_spaces/tests/trial/events.ts new file mode 100644 index 0000000000000..075c413e1f2dc --- /dev/null +++ b/x-pack/test/timeline/security_and_spaces/tests/trial/events.ts @@ -0,0 +1,183 @@ +/* + * 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 { JsonObject } from '@kbn/common-utils'; + +import { User } from '../../../../rule_registry/common/lib/authentication/types'; +import { TimelineEdges, TimelineNonEcsData } from '../../../../../plugins/timelines/common/'; +import { getSpaceUrlPrefix } from '../../../../rule_registry/common/lib/authentication/spaces'; + +import { + obsMinReadAlertsRead, + obsMinReadAlertsReadSpacesAll, + obsMinRead, + obsMinReadSpacesAll, +} from '../../../../rule_registry/common/lib/authentication/users'; +import { + Direction, + TimelineEventsQueries, +} from '../../../../../plugins/security_solution/common/search_strategy'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +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[]; +} + +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'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const getPostBody = (): JsonObject => ({ + defaultIndex: ['.alerts-*'], + entityType: 'alerts', + docValueFields: [ + { + field: '@timestamp', + }, + { + field: 'kibana.rac.alert.owner', + }, + { + field: 'kibana.rac.alert.id', + }, + { + field: 'event.kind', + }, + ], + factoryQueryType: TimelineEventsQueries.all, + fieldRequested: [ + '@timestamp', + 'message', + 'kibana.rac.alert.owner', + 'kibana.rac.alert.id', + 'event.kind', + ], + fields: [], + filterQuery: { + bool: { + filter: [ + { + match_all: {}, + }, + ], + }, + }, + pagination: { + activePage: 0, + querySize: 25, + }, + language: 'kuery', + sort: [ + { + field: '@timestamp', + direction: Direction.desc, + type: 'number', + }, + ], + timerange: { + from: FROM, + to: TO, + interval: '12h', + }, + }); + + describe('Timeline - Events', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + function addTests({ + space, + authorizedUsers, + 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: TimelineEdges) => { + const data: TimelineNonEcsData[] = hit.node.data; + return data.some(({ field, value }) => { + return ( + field === 'kibana.rac.alert.owner' && + featureIds.includes((value && value[0]) ?? '') + ); + }); + }) + ).to.equal(true); + expect(timeline.totalCount).to.be(expectedNumberAlerts); + }); + }); + + 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 }) + // TODO - This should be updated to be a 403 once this ticket is resolved + // https://github.com/elastic/kibana/issues/106005 + .expect(500); + }); + }); + } + + describe('alerts authentication', () => { + addTests({ + space: SPACE_1, + featureIds: ['apm'], + expectedNumberAlerts: 2, + body: { + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: 'alerts', + alertConsumers: ['apm'], + }, + authorizedUsers: [obsMinReadAlertsRead, obsMinReadAlertsReadSpacesAll], + unauthorizedUsers: [obsMinRead, obsMinReadSpacesAll], + }); + }); + }); +}; diff --git a/x-pack/test/timeline/security_and_spaces/tests/trial/index.ts b/x-pack/test/timeline/security_and_spaces/tests/trial/index.ts new file mode 100644 index 0000000000000..736fb6619c82d --- /dev/null +++ b/x-pack/test/timeline/security_and_spaces/tests/trial/index.ts @@ -0,0 +1,103 @@ +/* + * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createSpaces, + createUsersAndRoles, + deleteSpaces, + deleteUsersAndRoles, +} from '../../../../rule_registry/common/lib/authentication'; + +import { + observabilityMinReadAlertsRead, + observabilityMinReadAlertsReadSpacesAll, + observabilityMinimalRead, + observabilityMinimalReadSpacesAll, + observabilityMinReadAlertsAll, + observabilityMinReadAlertsAllSpacesAll, + observabilityMinimalAll, + observabilityMinimalAllSpacesAll, +} from '../../../../rule_registry/common/lib/authentication/roles'; +import { + obsMinReadAlertsRead, + obsMinReadAlertsReadSpacesAll, + obsMinRead, + obsMinReadSpacesAll, + superUser, + obsMinReadAlertsAll, + obsMinReadAlertsAllSpacesAll, + obsMinAll, + obsMinAllSpacesAll, +} from '../../../../rule_registry/common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile, getService }: FtrProviderContext): void => { + describe('timeline security and spaces enabled: trial', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + before(async () => { + await createSpaces(getService); + await createUsersAndRoles( + getService, + [ + obsMinReadAlertsRead, + obsMinReadAlertsReadSpacesAll, + obsMinRead, + obsMinReadSpacesAll, + superUser, + obsMinReadAlertsAll, + obsMinReadAlertsAllSpacesAll, + obsMinAll, + obsMinAllSpacesAll, + ], + [ + observabilityMinReadAlertsRead, + observabilityMinReadAlertsReadSpacesAll, + observabilityMinimalRead, + observabilityMinimalReadSpacesAll, + observabilityMinReadAlertsAll, + observabilityMinReadAlertsAllSpacesAll, + observabilityMinimalAll, + observabilityMinimalAllSpacesAll, + ] + ); + }); + + after(async () => { + await deleteSpaces(getService); + await deleteUsersAndRoles( + getService, + [ + obsMinReadAlertsRead, + obsMinReadAlertsReadSpacesAll, + obsMinRead, + obsMinReadSpacesAll, + superUser, + obsMinReadAlertsAll, + obsMinReadAlertsAllSpacesAll, + obsMinAll, + obsMinAllSpacesAll, + ], + [ + observabilityMinReadAlertsRead, + observabilityMinReadAlertsReadSpacesAll, + observabilityMinimalRead, + observabilityMinimalReadSpacesAll, + observabilityMinReadAlertsAll, + observabilityMinReadAlertsAllSpacesAll, + observabilityMinimalAll, + observabilityMinimalAllSpacesAll, + ] + ); + }); + + // Trial + loadTestFile(require.resolve('./events')); + }); +}; diff --git a/x-pack/test/timeline/security_only/config_basic.ts b/x-pack/test/timeline/security_only/config_basic.ts new file mode 100644 index 0000000000000..470b9097755f6 --- /dev/null +++ b/x-pack/test/timeline/security_only/config_basic.ts @@ -0,0 +1,16 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_only', { + license: 'basic', + disabledPlugins: ['spaces'], + ssl: false, + testFiles: [require.resolve('./tests/basic')], +}); diff --git a/x-pack/test/timeline/security_only/config_trial.ts b/x-pack/test/timeline/security_only/config_trial.ts new file mode 100644 index 0000000000000..8ca7dc950b78b --- /dev/null +++ b/x-pack/test/timeline/security_only/config_trial.ts @@ -0,0 +1,16 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_only', { + license: 'trial', + disabledPlugins: ['spaces'], + ssl: false, + testFiles: [require.resolve('./tests/trial')], +}); diff --git a/x-pack/test/timeline/security_only/tests/basic/events.ts b/x-pack/test/timeline/security_only/tests/basic/events.ts new file mode 100644 index 0000000000000..0f7ddc0c05825 --- /dev/null +++ b/x-pack/test/timeline/security_only/tests/basic/events.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 { JsonObject } from '@kbn/common-utils'; + +import { getSpaceUrlPrefix } from '../../../../rule_registry/common/lib/authentication/spaces'; + +import { + superUser, + globalRead, + secOnly, + secOnlyRead, + noKibanaPrivileges, +} from '../../../../rule_registry/common/lib/authentication/users'; +import { + Direction, + TimelineEventsQueries, +} from '../../../../../plugins/security_solution/common/search_strategy'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +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'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const getPostBody = (): JsonObject => ({ + defaultIndex: ['.alerts-*'], + entityType: 'alerts', + docValueFields: [ + { + field: '@timestamp', + }, + { + field: 'kibana.rac.alert.owner', + }, + { + field: 'kibana.rac.alert.id', + }, + { + field: 'event.kind', + }, + ], + factoryQueryType: TimelineEventsQueries.all, + fieldRequested: [ + '@timestamp', + 'message', + 'kibana.rac.alert.owner', + 'kibana.rac.alert.id', + 'event.kind', + ], + fields: [], + filterQuery: { + bool: { + filter: [ + { + match_all: {}, + }, + ], + }, + }, + pagination: { + activePage: 0, + querySize: 25, + }, + language: 'kuery', + sort: [ + { + field: '@timestamp', + direction: Direction.desc, + type: 'number', + }, + ], + timerange: { + from: FROM, + to: TO, + interval: '12h', + }, + }); + + describe('Timeline - Events', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + const authorizedSecSpace1 = [secOnly, secOnlyRead]; + const authorizedInAllSpaces = [superUser, globalRead]; + const unauthorized = [noKibanaPrivileges]; + + [...authorizedSecSpace1, ...authorizedInAllSpaces].forEach(({ username, password }) => { + it(`${username} - should return a 404 when accessing a spaces route`, async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE_1)}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send({ + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: 'alerts', + alertConsumers: ['siem'], + }) + .expect(404); + }); + }); + + [...authorizedInAllSpaces].forEach(({ username, password }) => { + it(`${username} - should return 200 for authorized users`, async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix()}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send({ + ...getPostBody(), + alertConsumers: ['siem', 'apm'], + }) + .expect(200); + }); + }); + + [...unauthorized].forEach(({ username, password }) => { + it(`${username} - should return 403 for unauthorized users`, async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix()}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send({ + ...getPostBody(), + alertConsumers: ['siem', 'apm'], + }) + // TODO - This should be updated to be a 403 once this ticket is resolved + // https://github.com/elastic/kibana/issues/106005 + .expect(500); + }); + }); + }); +}; diff --git a/x-pack/test/timeline/security_only/tests/basic/index.ts b/x-pack/test/timeline/security_only/tests/basic/index.ts new file mode 100644 index 0000000000000..60957c0956110 --- /dev/null +++ b/x-pack/test/timeline/security_only/tests/basic/index.ts @@ -0,0 +1,31 @@ +/* + * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createUsersAndRoles, + deleteUsersAndRoles, +} from '../../../../rule_registry/common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile, getService }: FtrProviderContext): void => { + describe('timeline security only: basic', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + before(async () => { + await createUsersAndRoles(getService); + }); + + after(async () => { + await deleteUsersAndRoles(getService); + }); + + // Basic + loadTestFile(require.resolve('./events')); + }); +}; diff --git a/x-pack/test/timeline/security_only/tests/trial/events.ts b/x-pack/test/timeline/security_only/tests/trial/events.ts new file mode 100644 index 0000000000000..0f7ddc0c05825 --- /dev/null +++ b/x-pack/test/timeline/security_only/tests/trial/events.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 { JsonObject } from '@kbn/common-utils'; + +import { getSpaceUrlPrefix } from '../../../../rule_registry/common/lib/authentication/spaces'; + +import { + superUser, + globalRead, + secOnly, + secOnlyRead, + noKibanaPrivileges, +} from '../../../../rule_registry/common/lib/authentication/users'; +import { + Direction, + TimelineEventsQueries, +} from '../../../../../plugins/security_solution/common/search_strategy'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +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'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const getPostBody = (): JsonObject => ({ + defaultIndex: ['.alerts-*'], + entityType: 'alerts', + docValueFields: [ + { + field: '@timestamp', + }, + { + field: 'kibana.rac.alert.owner', + }, + { + field: 'kibana.rac.alert.id', + }, + { + field: 'event.kind', + }, + ], + factoryQueryType: TimelineEventsQueries.all, + fieldRequested: [ + '@timestamp', + 'message', + 'kibana.rac.alert.owner', + 'kibana.rac.alert.id', + 'event.kind', + ], + fields: [], + filterQuery: { + bool: { + filter: [ + { + match_all: {}, + }, + ], + }, + }, + pagination: { + activePage: 0, + querySize: 25, + }, + language: 'kuery', + sort: [ + { + field: '@timestamp', + direction: Direction.desc, + type: 'number', + }, + ], + timerange: { + from: FROM, + to: TO, + interval: '12h', + }, + }); + + describe('Timeline - Events', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + const authorizedSecSpace1 = [secOnly, secOnlyRead]; + const authorizedInAllSpaces = [superUser, globalRead]; + const unauthorized = [noKibanaPrivileges]; + + [...authorizedSecSpace1, ...authorizedInAllSpaces].forEach(({ username, password }) => { + it(`${username} - should return a 404 when accessing a spaces route`, async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE_1)}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send({ + ...getPostBody(), + defaultIndex: ['.alerts-*'], + entityType: 'alerts', + alertConsumers: ['siem'], + }) + .expect(404); + }); + }); + + [...authorizedInAllSpaces].forEach(({ username, password }) => { + it(`${username} - should return 200 for authorized users`, async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix()}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send({ + ...getPostBody(), + alertConsumers: ['siem', 'apm'], + }) + .expect(200); + }); + }); + + [...unauthorized].forEach(({ username, password }) => { + it(`${username} - should return 403 for unauthorized users`, async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix()}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send({ + ...getPostBody(), + alertConsumers: ['siem', 'apm'], + }) + // TODO - This should be updated to be a 403 once this ticket is resolved + // https://github.com/elastic/kibana/issues/106005 + .expect(500); + }); + }); + }); +}; diff --git a/x-pack/test/timeline/security_only/tests/trial/index.ts b/x-pack/test/timeline/security_only/tests/trial/index.ts new file mode 100644 index 0000000000000..fbe8d3ec9ee0e --- /dev/null +++ b/x-pack/test/timeline/security_only/tests/trial/index.ts @@ -0,0 +1,31 @@ +/* + * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createUsersAndRoles, + deleteUsersAndRoles, +} from '../../../../rule_registry/common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile, getService }: FtrProviderContext): void => { + describe('timeline security only: trial', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + before(async () => { + await createUsersAndRoles(getService); + }); + + after(async () => { + await deleteUsersAndRoles(getService); + }); + + // Basic + loadTestFile(require.resolve('./events')); + }); +}; diff --git a/x-pack/test/timeline/spaces_only/config.ts b/x-pack/test/timeline/spaces_only/config.ts new file mode 100644 index 0000000000000..442ebed0c125c --- /dev/null +++ b/x-pack/test/timeline/spaces_only/config.ts @@ -0,0 +1,16 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('spaces_only', { + license: 'trial', + disabledPlugins: ['security'], + ssl: false, + testFiles: [require.resolve('./tests')], +}); diff --git a/x-pack/test/timeline/spaces_only/tests/events.ts b/x-pack/test/timeline/spaces_only/tests/events.ts new file mode 100644 index 0000000000000..71ea077406462 --- /dev/null +++ b/x-pack/test/timeline/spaces_only/tests/events.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 expect from '@kbn/expect'; +import { JsonObject } from '@kbn/common-utils'; + +import { FtrProviderContext } from '../../../rule_registry/common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../rule_registry/common/lib/authentication/spaces'; +import { + Direction, + TimelineEventsQueries, +} from '../../../../plugins/security_solution/common/search_strategy'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const TO = '3000-01-01T00:00:00.000Z'; + const FROM = '2000-01-01T00:00:00.000Z'; + const TEST_URL = '/internal/search/timelineSearchStrategy/'; + const SPACE1 = 'space1'; + const OTHER = 'other'; + + const getPostBody = (): JsonObject => ({ + defaultIndex: ['.alerts-*'], + entityType: 'alerts', + docValueFields: [ + { + field: '@timestamp', + }, + { + field: 'kibana.rac.alert.owner', + }, + { + field: 'kibana.rac.alert.id', + }, + { + field: 'event.kind', + }, + ], + factoryQueryType: TimelineEventsQueries.all, + fieldRequested: [ + '@timestamp', + 'message', + 'kibana.rac.alert.owner', + 'kibana.rac.alert.id', + 'event.kind', + ], + fields: [], + filterQuery: { + bool: { + filter: [ + { + match_all: {}, + }, + ], + }, + }, + pagination: { + activePage: 0, + querySize: 25, + }, + language: 'kuery', + sort: [ + { + field: '@timestamp', + direction: Direction.desc, + type: 'number', + }, + ], + timerange: { + from: FROM, + to: TO, + interval: '12h', + }, + }); + + describe('Timeline - Events', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + it('should handle alerts request appropriately', async () => { + const resp = await supertest + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send({ + ...getPostBody(), + alertConsumers: ['siem', 'apm'], + }) + .expect(200); + + // there's 5 total alerts, one is assigned to space2 only + expect(resp.body.totalCount).to.be(4); + }); + + it('should not return alerts from another space', async () => { + const resp = await supertest + .post(`${getSpaceUrlPrefix(OTHER)}${TEST_URL}`) + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send({ + ...getPostBody(), + alertConsumers: ['siem', 'apm'], + }) + .expect(200); + + expect(resp.body.totalCount).to.be(0); + }); + }); +}; diff --git a/x-pack/test/timeline/spaces_only/tests/index.ts b/x-pack/test/timeline/spaces_only/tests/index.ts new file mode 100644 index 0000000000000..857ca027a2371 --- /dev/null +++ b/x-pack/test/timeline/spaces_only/tests/index.ts @@ -0,0 +1,28 @@ +/* + * 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 { FtrProviderContext } from '../../common/ftr_provider_context'; +import { createSpaces, deleteSpaces } from '../../../rule_registry/common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile, getService }: FtrProviderContext): void => { + describe('timeline spaces only: trial', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + before(async () => { + await createSpaces(getService); + }); + + after(async () => { + await deleteSpaces(getService); + }); + + // Basic + loadTestFile(require.resolve('./events')); + }); +};