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