[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:
parent
196eb7b6a9
commit
44a9dadaff
|
@ -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) => IScopedSearchClient</code> | |
|
||||
| [getSearchStrategy](./kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md) | <code>(name?: string) => ISearchStrategy<SearchStrategyRequest, SearchStrategyResponse></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) => Promise<ISearchStartSearchSource>;</code><br/><code> }</code> | |
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) > [searchAsInternalUser](./kibana-plugin-plugins-data-server.isearchstart.searchasinternaluser.md)
|
||||
|
||||
## ISearchStart.searchAsInternalUser property
|
||||
|
||||
Search as the internal Kibana system user. This is not a registered search strategy as we don't want to allow access from the client.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
searchAsInternalUser: ISearchStrategy;
|
||||
```
|
|
@ -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',
|
|
@ -7,3 +7,4 @@
|
|||
*/
|
||||
|
||||
export * from './technical_field_names';
|
||||
export * from './alerts_as_data_rbac';
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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: [],
|
||||
});
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: () => {},
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()', () => {
|
||||
|
|
|
@ -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()', () => {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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' });
|
||||
});
|
||||
|
|
|
@ -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' \
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -16,3 +16,8 @@ export enum TimelineEventsQueries {
|
|||
kpi = 'eventsKpi',
|
||||
lastEventTime = 'eventsLastEventTime',
|
||||
}
|
||||
|
||||
export enum EntityType {
|
||||
ALERTS = 'alerts',
|
||||
EVENTS = 'events',
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -6,6 +6,6 @@
|
|||
"extraPublicDirs": ["common"],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["data", "dataEnhanced", "kibanaReact", "kibanaUtils"],
|
||||
"requiredPlugins": ["alerting", "data", "dataEnhanced", "kibanaReact", "kibanaUtils"],
|
||||
"optionalPlugins": []
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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" }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
386
x-pack/test/api_integration/apis/security_solution/utils.ts
Normal file
386
x-pack/test/api_integration/apis/security_solution/utils.ts
Normal 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',
|
||||
},
|
||||
];
|
|
@ -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: {
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}` : ``;
|
||||
|
|
|
@ -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);
|
||||
|
|
97
x-pack/test/timeline/common/config.ts
Normal file
97
x-pack/test/timeline/common/config.ts
Normal 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}`,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
12
x-pack/test/timeline/common/ftr_provider_context.d.ts
vendored
Normal file
12
x-pack/test/timeline/common/ftr_provider_context.d.ts
vendored
Normal 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, {}>;
|
8
x-pack/test/timeline/common/services.ts
Normal file
8
x-pack/test/timeline/common/services.ts
Normal 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';
|
15
x-pack/test/timeline/security_and_spaces/config_basic.ts
Normal file
15
x-pack/test/timeline/security_and_spaces/config_basic.ts
Normal 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')],
|
||||
});
|
15
x-pack/test/timeline/security_and_spaces/config_trial.ts
Normal file
15
x-pack/test/timeline/security_and_spaces/config_trial.ts
Normal 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')],
|
||||
});
|
408
x-pack/test/timeline/security_and_spaces/tests/basic/events.ts
Normal file
408
x-pack/test/timeline/security_and_spaces/tests/basic/events.ts
Normal 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],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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'));
|
||||
});
|
||||
};
|
183
x-pack/test/timeline/security_and_spaces/tests/trial/events.ts
Normal file
183
x-pack/test/timeline/security_and_spaces/tests/trial/events.ts
Normal 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],
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
103
x-pack/test/timeline/security_and_spaces/tests/trial/index.ts
Normal file
103
x-pack/test/timeline/security_and_spaces/tests/trial/index.ts
Normal 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'));
|
||||
});
|
||||
};
|
16
x-pack/test/timeline/security_only/config_basic.ts
Normal file
16
x-pack/test/timeline/security_only/config_basic.ts
Normal 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')],
|
||||
});
|
16
x-pack/test/timeline/security_only/config_trial.ts
Normal file
16
x-pack/test/timeline/security_only/config_trial.ts
Normal 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')],
|
||||
});
|
149
x-pack/test/timeline/security_only/tests/basic/events.ts
Normal file
149
x-pack/test/timeline/security_only/tests/basic/events.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
31
x-pack/test/timeline/security_only/tests/basic/index.ts
Normal file
31
x-pack/test/timeline/security_only/tests/basic/index.ts
Normal 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'));
|
||||
});
|
||||
};
|
149
x-pack/test/timeline/security_only/tests/trial/events.ts
Normal file
149
x-pack/test/timeline/security_only/tests/trial/events.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
31
x-pack/test/timeline/security_only/tests/trial/index.ts
Normal file
31
x-pack/test/timeline/security_only/tests/trial/index.ts
Normal 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'));
|
||||
});
|
||||
};
|
16
x-pack/test/timeline/spaces_only/config.ts
Normal file
16
x-pack/test/timeline/spaces_only/config.ts
Normal 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')],
|
||||
});
|
121
x-pack/test/timeline/spaces_only/tests/events.ts
Normal file
121
x-pack/test/timeline/spaces_only/tests/events.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
};
|
28
x-pack/test/timeline/spaces_only/tests/index.ts
Normal file
28
x-pack/test/timeline/spaces_only/tests/index.ts
Normal 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'));
|
||||
});
|
||||
};
|
Loading…
Reference in a new issue