[Timeline][RBAC] - Add RBAC logic to timeline alerts search strategy (#105333)

## Summary
 
Adds RBAC layer to timeline plugin search strategy for alerts.
This commit is contained in:
Yara Tercero 2021-07-27 22:36:54 -07:00 committed by GitHub
parent 196eb7b6a9
commit 44a9dadaff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 2496 additions and 605 deletions

View file

@ -17,5 +17,6 @@ export interface ISearchStart<SearchStrategyRequest extends IKibanaSearchRequest
| [aggs](./kibana-plugin-plugins-data-server.isearchstart.aggs.md) | <code>AggsStart</code> | |
| [asScoped](./kibana-plugin-plugins-data-server.isearchstart.asscoped.md) | <code>(request: KibanaRequest) =&gt; IScopedSearchClient</code> | |
| [getSearchStrategy](./kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md) | <code>(name?: string) =&gt; ISearchStrategy&lt;SearchStrategyRequest, SearchStrategyResponse&gt;</code> | 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) | <code>ISearchStrategy</code> | 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) | <code>{</code><br/><code> asScoped: (request: KibanaRequest) =&gt; Promise&lt;ISearchStartSearchSource&gt;;</code><br/><code> }</code> | |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) &gt; [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.
<b>Signature:</b>
```typescript
searchAsInternalUser: ISearchStrategy;
```

View file

@ -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<ALERTS_CONSUMERS, string | string[]> = {
apm: '.alerts-observability-apm',
logs: '.alerts-observability.logs',
infrastructure: '.alerts-observability.metrics',

View file

@ -7,3 +7,4 @@
*/
export * from './technical_field_names';
export * from './alerts_as_data_rbac';

View file

@ -23,6 +23,7 @@ export function createSearchSetupMock(): jest.Mocked<ISearchSetup> {
export function createSearchStartMock(): jest.Mocked<ISearchStart> {
return {
aggs: searchAggsStartMock(),
searchAsInternalUser: createSearchRequestHandlerContext(),
getSearchStrategy: jest.fn(),
asScoped: jest.fn().mockReturnValue(createSearchRequestHandlerContext()),
searchSource: searchSourceMock.createStartContract(),

View file

@ -109,6 +109,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
private searchStrategies: StrategyMap = {};
private sessionService: ISearchSessionService;
private asScoped!: ISearchStart['asScoped'];
private searchAsInternalUser!: ISearchStrategy;
constructor(
private initializerContext: PluginInitializerContext<ConfigSchema>,
@ -156,6 +157,17 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
)
);
// 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<ISearchSetup, ISearchStart> {
uiSettings,
indexPatterns,
}),
searchAsInternalUser: this.searchAsInternalUser,
getSearchStrategy: this.getSearchStrategy,
asScoped: this.asScoped,
searchSource: {

View file

@ -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

View file

@ -1048,6 +1048,7 @@ export interface ISearchStart<SearchStrategyRequest extends IKibanaSearchRequest
// (undocumented)
asScoped: (request: KibanaRequest) => IScopedSearchClient;
getSearchStrategy: (name?: string) => ISearchStrategy<SearchStrategyRequest, SearchStrategyResponse>;
searchAsInternalUser: ISearchStrategy;
// (undocumented)
searchSource: {
asScoped: (request: KibanaRequest) => Promise<ISearchStartSearchSource>;
@ -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)

View file

@ -29,6 +29,7 @@ const securityPluginStart = securityMock.createStart();
const alertingAuthorizationClientFactoryParams: jest.Mocked<AlertingAuthorizationClientFactoryOpts> = {
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: [],
});

View file

@ -19,6 +19,7 @@ export interface AlertingAuthorizationClientFactoryOpts {
securityPluginSetup?: SecurityPluginSetup;
securityPluginStart?: SecurityPluginStart;
getSpace: (request: KibanaRequest) => Promise<Space | undefined>;
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<Space | undefined>;
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(

View file

@ -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);

View file

@ -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<Space | undefined>;
getSpaceId: (request: KibanaRequest) => string | undefined;
auditLogger: AlertingAuthorizationAuditLogger;
exemptConsumerIds: string[];
authorization?: SecurityPluginSetup['authz'];
@ -81,7 +83,7 @@ export class AlertingAuthorization {
private readonly featuresIds: Promise<Set<string>>;
private readonly allPossibleConsumers: Promise<AuthorizedConsumers>;
private readonly exemptConsumerIds: string[];
private readonly spaceId: Promise<string | undefined>;
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<string | undefined> {
public getSpaceId(): string | undefined {
return this.spaceId;
}
@ -303,7 +306,7 @@ export class AlertingAuthorization {
const authorizedEntries: Map<string, Set<string>> = 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: () => {},
};

View file

@ -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(

View file

@ -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<RegistryAlertTypeWithAuth>,
opts: AlertingAuthorizationFilterOpts
opts: AlertingAuthorizationFilterOpts,
spaceId: string | undefined
): KueryNode | JsonObject {
const kueryNode = nodeBuilder.or(
Array.from(ruleTypes).reduce<KueryNode[]>((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) {

View file

@ -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,
});

View file

@ -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<Obj extends {}, Props extends keyof Obj> = Omit<Obj, Props> &
@ -63,13 +67,15 @@ export class AlertsClient {
private readonly auditLogger?: AuditLogger;
private readonly authorization: PublicMethodsOf<AlertingAuthorization>;
private readonly esClient: ElasticsearchClient;
private readonly spaceId: Promise<string | undefined>;
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;

View file

@ -27,7 +27,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
beforeEach(() => {
jest.resetAllMocks();
alertingAuthMock.getSpaceId.mockImplementation(() => Promise.resolve('test_default_space_id'));
alertingAuthMock.getSpaceId.mockImplementation(() => 'test_default_space_id');
});
describe('get()', () => {

View file

@ -27,7 +27,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
beforeEach(() => {
jest.resetAllMocks();
alertingAuthMock.getSpaceId.mockImplementation(() => Promise.resolve('test_default_space_id'));
alertingAuthMock.getSpaceId.mockImplementation(() => 'test_default_space_id');
});
describe('update()', () => {

View file

@ -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<RacRequestHandlerContext>) => {
router.get(

View file

@ -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<TSearchRequest extends ESSearchRequest>(

View file

@ -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;

View file

@ -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';

View file

@ -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' });
});

View file

@ -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' \

View file

@ -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<PluginSetup, PluginStart, SetupPlugins, S
settings: {
number_of_shards: 1,
},
mappings: {}, // TODO: Add mappings here via `mappingFromFieldMap()`
mappings: { dynamic: false, ...mappingFromFieldMap(technicalRuleFieldMap) },
},
},
});
@ -270,6 +273,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
const referenceRuleTypes = [
REFERENCE_RULE_ALERT_TYPE_ID,
REFERENCE_RULE_PERSISTENCE_ALERT_TYPE_ID,
CUSTOM_ALERT_TYPE_ID,
];
const ruleTypes = [
SIGNALS_ID,

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import { JsonObject } from '@kbn/common-utils';
import type { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
import type { Ecs } from '../../../../ecs';
import type { CursorType, Inspect, Maybe, PaginationInputPaginated } from '../../../common';
@ -39,4 +41,5 @@ export interface TimelineEventsAllRequestOptions extends TimelineRequestOptionsP
fieldRequested: string[];
language: 'eql' | 'kuery' | 'lucene';
excludeEcsData?: boolean;
authFilter?: JsonObject;
}

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import { JsonObject } from '@kbn/common-utils';
import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
import { Inspect, Maybe } from '../../../common';
import { TimelineRequestOptionsPaginated } from '../..';
@ -28,4 +30,5 @@ export interface TimelineEventsDetailsRequestOptions
extends Partial<TimelineRequestOptionsPaginated> {
indexName: string;
eventId: string;
authFilter?: JsonObject;
}

View file

@ -16,3 +16,8 @@ export enum TimelineEventsQueries {
kpi = 'eventsKpi',
lastEventTime = 'eventsLastEventTime',
}
export enum EntityType {
ALERTS = 'alerts',
EVENTS = 'events',
}

View file

@ -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<Field = string> extends SortField<Field> {

View file

@ -6,6 +6,6 @@
"extraPublicDirs": ["common"],
"server": true,
"ui": true,
"requiredPlugins": ["data", "dataEnhanced", "kibanaReact", "kibanaUtils"],
"requiredPlugins": ["alerting", "data", "dataEnhanced", "kibanaReact", "kibanaUtils"],
"optionalPlugins": []
}

View file

@ -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();

View file

@ -22,13 +22,13 @@ import { buildFieldsRequest, formatTimelineData } from './helpers';
import { inspectStringifyObject } from '../../../../../utils/build_query';
export const timelineEventsAll: TimelineFactory<TimelineEventsQueries.all> = {
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,

View file

@ -23,6 +23,7 @@ export const buildTimelineEventsAllQuery = ({
pagination: { activePage, querySize },
sort,
timerange,
authFilter,
}: Omit<TimelineEventsAllRequestOptions, 'fieldRequested'>) => {
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) => {

View file

@ -26,9 +26,9 @@ import {
} from '../../../../../../common/utils/field_formatters';
export const timelineEventsDetails: TimelineFactory<TimelineEventsQueries.details> = {
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,

View file

@ -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,
};
};

View file

@ -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;
};

View file

@ -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<T extends TimelineFactoryQueryTypes> {
buildDsl: (options: TimelineStrategyRequestType<T>) => unknown;
buildDsl: (options: TimelineStrategyRequestType<T>) => ISearchRequestParams;
parse: (
options: TimelineStrategyRequestType<T>,
response: IEsSearchResponse

View file

@ -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 = <T extends TimelineFactoryQueryTypes>(
data: PluginStart
data: PluginStart,
alerting: AlertingPluginStartContract
): ISearchStrategy<TimelineStrategyRequestType<T>, TimelineStrategyResponseType<T>> => {
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<T> = 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<T> = 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 = <T extends TimelineFactoryQueryTyp
},
};
};
const timelineSearchStrategy = <T extends TimelineFactoryQueryTypes>({
es,
request,
options,
deps,
queryFactory,
}: {
es: ISearchStrategy;
request: TimelineStrategyRequestType<T>;
options: ISearchOptions;
deps: SearchStrategyDependencies;
queryFactory: TimelineFactory<T>;
}) => {
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 = <T extends TimelineFactoryQueryTypes>({
es,
request,
options,
deps,
queryFactory,
alerting,
alertConsumers,
}: {
es: ISearchStrategy;
request: TimelineStrategyRequestType<T>;
options: ISearchOptions;
deps: SearchStrategyDependencies;
alerting: AlertingPluginStartContract;
queryFactory: TimelineFactory<T>;
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;
})
);
};

View file

@ -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;
}

View file

@ -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" }
]
}

View file

@ -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);
})
.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);
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]);
});
});
}

View file

@ -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',
},
];

View file

@ -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: {

View file

@ -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"]
}
}
}

View file

@ -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}` : ``;

View file

@ -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);

View file

@ -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}`,
]
: []),
],
},
};
};
}

View file

@ -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<typeof services, {}>;

View file

@ -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';

View file

@ -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')],
});

View file

@ -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')],
});

View file

@ -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],
});
});
});
});
});
};

View file

@ -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'));
});
};

View file

@ -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],
});
});
});
};

View file

@ -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'));
});
};

View file

@ -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')],
});

View file

@ -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')],
});

View file

@ -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);
});
});
});
};

View file

@ -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'));
});
};

View file

@ -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);
});
});
});
};

View file

@ -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'));
});
};

View file

@ -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')],
});

View file

@ -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);
});
});
};

View file

@ -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'));
});
};