mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security solution] [Endpoint] Use internal ES user in get index patterns and autosuggestions for event filters form (#145883)
## Summary - Cretes new search strategy for getting index patterns in security solution plugin that uses the internal ES user in order to retrieve event filters fields without having extra index privileges. - Adds new API endpoint for autocomplete suggestions that uses unified_search plugin logic but using the internal ES user. - Updates frontend code to use both approaches above in event filters form. - Adds new unit tests ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
8c7efbba54
commit
2b3755e395
18 changed files with 902 additions and 153 deletions
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { clone } from 'lodash';
|
||||
import { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/server';
|
||||
import { registerRoutes } from './routes';
|
||||
import { ConfigSchema } from '../../config';
|
||||
|
@ -32,6 +33,7 @@ export class AutocompleteService implements Plugin<void> {
|
|||
terminateAfter: moment.duration(terminateAfter).asMilliseconds(),
|
||||
timeout: moment.duration(timeout).asMilliseconds(),
|
||||
}),
|
||||
getInitializerContextConfig: () => clone(this.initializerContext.config),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -6,10 +6,32 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ConfigSchema } from '../config';
|
||||
import { AutocompleteSetup } from './autocomplete';
|
||||
|
||||
const autocompleteSetupMock: jest.Mocked<AutocompleteSetup> = {
|
||||
getAutocompleteSettings: jest.fn(),
|
||||
// @ts-ignore as it is partially defined because not all fields are needed
|
||||
getInitializerContextConfig: jest.fn(() => ({
|
||||
create: jest.fn(
|
||||
() =>
|
||||
new Observable<ConfigSchema>((subscribe) =>
|
||||
subscribe.next({
|
||||
autocomplete: {
|
||||
querySuggestions: { enabled: true },
|
||||
valueSuggestions: {
|
||||
enabled: true,
|
||||
tiers: [],
|
||||
terminateAfter: moment.duration(),
|
||||
timeout: moment.duration(),
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
),
|
||||
})),
|
||||
};
|
||||
|
||||
function createSetupContract() {
|
||||
|
|
|
@ -57,6 +57,9 @@ export const BASE_POLICY_RESPONSE_ROUTE = `${BASE_ENDPOINT_ROUTE}/policy_respons
|
|||
export const BASE_POLICY_ROUTE = `${BASE_ENDPOINT_ROUTE}/policy`;
|
||||
export const AGENT_POLICY_SUMMARY_ROUTE = `${BASE_POLICY_ROUTE}/summaries`;
|
||||
|
||||
/** Suggestions routes */
|
||||
export const SUGGESTIONS_ROUTE = `${BASE_ENDPOINT_ROUTE}/suggestions/{suggestion_type}`;
|
||||
|
||||
/** Host Isolation Routes */
|
||||
export const ISOLATE_HOST_ROUTE = `${BASE_ENDPOINT_ROUTE}/isolate`;
|
||||
export const UNISOLATE_HOST_ROUTE = `${BASE_ENDPOINT_ROUTE}/unisolate`;
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 type { TypeOf } from '@kbn/config-schema';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const EndpointSuggestionsSchema = {
|
||||
body: schema.object({
|
||||
field: schema.string(),
|
||||
query: schema.string(),
|
||||
filters: schema.maybe(schema.any()),
|
||||
fieldMeta: schema.maybe(schema.any()),
|
||||
}),
|
||||
params: schema.object({
|
||||
// Ready to be used with other suggestion types like endpoints
|
||||
suggestion_type: schema.oneOf([schema.literal('eventFilters')]),
|
||||
}),
|
||||
};
|
||||
|
||||
export type EndpointSuggestionsBody = TypeOf<typeof EndpointSuggestionsSchema.body>;
|
|
@ -98,7 +98,8 @@ interface FetchIndexReturn {
|
|||
*/
|
||||
export const useFetchIndex = (
|
||||
indexNames: string[],
|
||||
onlyCheckIfIndicesExist: boolean = false
|
||||
onlyCheckIfIndicesExist: boolean = false,
|
||||
strategy: string = 'indexFields'
|
||||
): [boolean, FetchIndexReturn] => {
|
||||
const { data } = useKibana().services;
|
||||
const abortCtrl = useRef(new AbortController());
|
||||
|
@ -124,7 +125,7 @@ export const useFetchIndex = (
|
|||
{ indices: iNames, onlyCheckIfIndicesExist },
|
||||
{
|
||||
abortSignal: abortCtrl.current.signal,
|
||||
strategy: 'indexFields',
|
||||
strategy,
|
||||
}
|
||||
)
|
||||
.subscribe({
|
||||
|
@ -169,7 +170,7 @@ export const useFetchIndex = (
|
|||
abortCtrl.current.abort();
|
||||
asyncSearch();
|
||||
},
|
||||
[data.search, addError, addWarning, onlyCheckIfIndicesExist, setLoading, setState]
|
||||
[data.search, addError, addWarning, onlyCheckIfIndicesExist, setLoading, setState, strategy]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import type { AutocompleteStart } from '@kbn/unified-search-plugin/public/autocomplete';
|
||||
import type { ValueSuggestionsGetFn } from '@kbn/unified-search-plugin/public/autocomplete/providers/value_suggestion_provider';
|
||||
|
||||
/**
|
||||
* Hook to get a memoized suggestions interface
|
||||
*/
|
||||
export function useSuggestions(fn: ValueSuggestionsGetFn): AutocompleteStart {
|
||||
return useMemo(
|
||||
() => ({
|
||||
getQuerySuggestions: () => undefined,
|
||||
hasQuerySuggestions: (_) => false,
|
||||
getValueSuggestions: fn,
|
||||
}),
|
||||
[fn]
|
||||
);
|
||||
}
|
|
@ -7,6 +7,9 @@
|
|||
|
||||
import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants';
|
||||
import type { HttpStart } from '@kbn/core/public';
|
||||
import type { EndpointSuggestionsBody } from '../../../../../common/endpoint/schema/suggestions';
|
||||
import { SUGGESTIONS_ROUTE } from '../../../../../common/endpoint/constants';
|
||||
import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables';
|
||||
import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client';
|
||||
import { EVENT_FILTER_LIST_DEFINITION } from '../constants';
|
||||
|
||||
|
@ -23,4 +26,18 @@ export class EventFiltersApiClient extends ExceptionsListApiClient {
|
|||
public static getInstance(http: HttpStart): ExceptionsListApiClient {
|
||||
return super.getInstance(http, ENDPOINT_EVENT_FILTERS_LIST_ID, EVENT_FILTER_LIST_DEFINITION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns suggestions for given field
|
||||
*/
|
||||
async getSuggestions(body: EndpointSuggestionsBody): Promise<string[]> {
|
||||
const result: string[] = await this.getHttp().post(
|
||||
resolvePathVariables(SUGGESTIONS_ROUTE, { suggestion_type: 'eventFilters' }),
|
||||
{
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,9 @@ import { OperatingSystem } from '@kbn/securitysolution-utils';
|
|||
|
||||
import { getExceptionBuilderComponentLazy } from '@kbn/lists-plugin/public';
|
||||
import type { OnChangeProps } from '@kbn/lists-plugin/public';
|
||||
import type { ValueSuggestionsGetFn } from '@kbn/unified-search-plugin/public/autocomplete/providers/value_suggestion_provider';
|
||||
import { eventsIndexPattern } from '../../../../../../common/endpoint/constants';
|
||||
import { useSuggestions } from '../../../../hooks/use_suggestions';
|
||||
import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator';
|
||||
import type { PolicyData } from '../../../../../../common/endpoint/types';
|
||||
import { useFetchIndex } from '../../../../../common/containers/source';
|
||||
|
@ -57,6 +60,7 @@ import { EffectedPolicySelect } from '../../../../components/effected_policy_sel
|
|||
import { isGlobalPolicyEffected } from '../../../../components/effected_policy_select/utils';
|
||||
import { ExceptionItemComments } from '../../../../../detection_engine/rule_exceptions/components/item_comments';
|
||||
import { filterIndexPatterns } from '../../../../../detection_engine/rule_exceptions/utils/helpers';
|
||||
import { EventFiltersApiClient } from '../../service/api_client';
|
||||
|
||||
const OPERATING_SYSTEMS: readonly OperatingSystem[] = [
|
||||
OperatingSystem.MAC,
|
||||
|
@ -113,8 +117,17 @@ type EventFilterItemEntries = Array<{
|
|||
export const EventFiltersForm: React.FC<ArtifactFormComponentProps & { allowSelectOs?: boolean }> =
|
||||
memo(({ allowSelectOs = true, item: exception, policies, policiesIsLoading, onChange, mode }) => {
|
||||
const getTestId = useTestIdGenerator('eventFilters-form');
|
||||
const { http, unifiedSearch } = useKibana().services;
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const getSuggestionsFn = useCallback<ValueSuggestionsGetFn>(
|
||||
({ field, query }) => {
|
||||
const eventFiltersAPIClient = new EventFiltersApiClient(http);
|
||||
return eventFiltersAPIClient.getSuggestions({ field: field.name, query });
|
||||
},
|
||||
[http]
|
||||
);
|
||||
|
||||
const autocompleteSuggestions = useSuggestions(getSuggestionsFn);
|
||||
const [hasFormChanged, setHasFormChanged] = useState(false);
|
||||
const [hasNameError, toggleHasNameError] = useState<boolean>(!exception.name);
|
||||
const [newComment, setNewComment] = useState('');
|
||||
|
@ -129,8 +142,13 @@ export const EventFiltersForm: React.FC<ArtifactFormComponentProps & { allowSele
|
|||
|
||||
const [hasDuplicateFields, setHasDuplicateFields] = useState<boolean>(false);
|
||||
// This value has to be memoized to avoid infinite useEffect loop on useFetchIndex
|
||||
const indexNames = useMemo(() => ['logs-endpoint.events.*'], []);
|
||||
const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(indexNames);
|
||||
const indexNames = useMemo(() => [eventsIndexPattern], []);
|
||||
const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(
|
||||
indexNames,
|
||||
undefined,
|
||||
'eventFiltersFields'
|
||||
);
|
||||
|
||||
const [areConditionsValid, setAreConditionsValid] = useState(
|
||||
!!exception.entries.length || false
|
||||
);
|
||||
|
@ -416,7 +434,7 @@ export const EventFiltersForm: React.FC<ArtifactFormComponentProps & { allowSele
|
|||
getExceptionBuilderComponentLazy({
|
||||
allowLargeValueLists: false,
|
||||
httpService: http,
|
||||
autocompleteService: unifiedSearch.autocomplete,
|
||||
autocompleteService: autocompleteSuggestions,
|
||||
exceptionListItems: [eventFilterItem as ExceptionListItemSchema],
|
||||
listType: EVENT_FILTER_LIST_TYPE,
|
||||
listId: ENDPOINT_EVENT_FILTERS_LIST_ID,
|
||||
|
@ -434,7 +452,14 @@ export const EventFiltersForm: React.FC<ArtifactFormComponentProps & { allowSele
|
|||
operatorsList: EVENT_FILTERS_OPERATORS,
|
||||
osTypes: exception.os_types,
|
||||
}),
|
||||
[unifiedSearch, handleOnBuilderChange, http, indexPatterns, exception, eventFilterItem]
|
||||
[
|
||||
autocompleteSuggestions,
|
||||
handleOnBuilderChange,
|
||||
http,
|
||||
indexPatterns,
|
||||
exception,
|
||||
eventFilterItem,
|
||||
]
|
||||
);
|
||||
|
||||
// conditions
|
||||
|
|
|
@ -94,6 +94,10 @@ export class ExceptionsListApiClient {
|
|||
return this.http === coreHttp;
|
||||
}
|
||||
|
||||
protected getHttp(): HttpStart {
|
||||
return this.http;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to get a fresh or existing instance.
|
||||
* It will ensure we only check and create the list once.
|
||||
|
|
|
@ -42,6 +42,7 @@ import { createCasesClientMock } from '@kbn/cases-plugin/server/client/mocks';
|
|||
import { createFleetAuthzMock } from '@kbn/fleet-plugin/common';
|
||||
import type { RequestFixtureOptions } from '@kbn/core-http-router-server-mocks';
|
||||
import type { ElasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
|
||||
import { getEndpointAuthzInitialStateMock } from '../../common/endpoint/service/authz/mocks';
|
||||
import { xpackMocks } from '../fixtures';
|
||||
import { createMockConfig, requestContextMock } from '../lib/detection_engine/routes/__mocks__';
|
||||
import type {
|
||||
|
@ -94,6 +95,7 @@ export const createMockEndpointAppContextService = (
|
|||
getManifestManager: jest.fn().mockReturnValue(mockManifestManager ?? jest.fn()),
|
||||
getEndpointMetadataService: jest.fn(() => mockEndpointMetadataContext.endpointMetadataService),
|
||||
getInternalFleetServices: jest.fn(() => mockEndpointMetadataContext.fleetServices),
|
||||
getEndpointAuthz: jest.fn(getEndpointAuthzInitialStateMock),
|
||||
} as unknown as jest.Mocked<EndpointAppContextService>;
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* 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 type { TypeOf } from '@kbn/config-schema';
|
||||
import type { ScopedClusterClientMock } from '@kbn/core/server/mocks';
|
||||
import {
|
||||
loggingSystemMock,
|
||||
elasticsearchServiceMock,
|
||||
savedObjectsClientMock,
|
||||
httpServerMock,
|
||||
httpServiceMock,
|
||||
} from '@kbn/core/server/mocks';
|
||||
import type { KibanaResponseFactory, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import type { ConfigSchema } from '@kbn/unified-search-plugin/config';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { dataPluginMock } from '@kbn/unified-search-plugin/server/mocks';
|
||||
import { termsEnumSuggestions } from '@kbn/unified-search-plugin/server/autocomplete/terms_enum';
|
||||
import {
|
||||
createMockEndpointAppContext,
|
||||
createMockEndpointAppContextServiceStartContract,
|
||||
createRouteHandlerContext,
|
||||
} from '../../mocks';
|
||||
import type { EndpointAuthz } from '../../../../common/endpoint/types/authz';
|
||||
import { applyActionsEsSearchMock } from '../../services/actions/mocks';
|
||||
import {
|
||||
createMockConfig,
|
||||
requestContextMock,
|
||||
} from '../../../lib/detection_engine/routes/__mocks__';
|
||||
import type { EndpointSuggestionsSchema } from '../../../../common/endpoint/schema/suggestions';
|
||||
import {
|
||||
getEndpointSuggestionsRequestHandler,
|
||||
registerEndpointSuggestionsRoutes,
|
||||
getLogger,
|
||||
} from '.';
|
||||
import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator';
|
||||
import { getEndpointAuthzInitialStateMock } from '../../../../common/endpoint/service/authz/mocks';
|
||||
import { eventsIndexPattern, SUGGESTIONS_ROUTE } from '../../../../common/endpoint/constants';
|
||||
import { EndpointAppContextService } from '../../endpoint_app_context_services';
|
||||
import { parseExperimentalConfigValue } from '../../../../common/experimental_features';
|
||||
|
||||
jest.mock('@kbn/unified-search-plugin/server/autocomplete/terms_enum', () => {
|
||||
return {
|
||||
termsEnumSuggestions: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const termsEnumSuggestionsMock = termsEnumSuggestions as jest.Mock;
|
||||
|
||||
interface CallRouteInterface {
|
||||
params: TypeOf<typeof EndpointSuggestionsSchema.params>;
|
||||
authz?: Partial<EndpointAuthz>;
|
||||
}
|
||||
|
||||
describe('when calling the Suggestions route handler', () => {
|
||||
let mockScopedEsClient: ScopedClusterClientMock;
|
||||
let mockSavedObjectClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
let mockResponse: jest.Mocked<KibanaResponseFactory>;
|
||||
let suggestionsRouteHandler: ReturnType<typeof getEndpointSuggestionsRequestHandler>;
|
||||
let callRoute: (
|
||||
routePrefix: string,
|
||||
opts: CallRouteInterface,
|
||||
indexExists?: { endpointDsExists: boolean }
|
||||
) => Promise<void>;
|
||||
|
||||
let config$: Observable<ConfigSchema>;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockContext = createMockEndpointAppContext();
|
||||
(mockContext.service.getEndpointMetadataService as jest.Mock) = jest.fn().mockReturnValue({
|
||||
findHostMetadataForFleetAgents: jest.fn().mockResolvedValue([]),
|
||||
});
|
||||
mockScopedEsClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockSavedObjectClient = savedObjectsClientMock.create();
|
||||
mockResponse = httpServerMock.createResponseFactory();
|
||||
|
||||
config$ = dataPluginMock
|
||||
.createSetupContract()
|
||||
.autocomplete.getInitializerContextConfig()
|
||||
.create();
|
||||
suggestionsRouteHandler = getEndpointSuggestionsRequestHandler(config$, getLogger(mockContext));
|
||||
});
|
||||
|
||||
describe('having right privileges', () => {
|
||||
it('should call service using event filters type from request', async () => {
|
||||
applyActionsEsSearchMock(mockScopedEsClient.asInternalUser);
|
||||
|
||||
const mockContext = requestContextMock.convertContext(
|
||||
createRouteHandlerContext(mockScopedEsClient, mockSavedObjectClient)
|
||||
);
|
||||
|
||||
const mockRequest = httpServerMock.createKibanaRequest<
|
||||
TypeOf<typeof EndpointSuggestionsSchema.params>,
|
||||
never,
|
||||
never
|
||||
>({
|
||||
params: { suggestion_type: 'eventFilters' },
|
||||
body: {
|
||||
field: 'test-field',
|
||||
query: 'test-query',
|
||||
filters: 'test-filters',
|
||||
fieldMeta: 'test-field-meta',
|
||||
},
|
||||
});
|
||||
|
||||
await suggestionsRouteHandler(mockContext, mockRequest, mockResponse);
|
||||
|
||||
expect(termsEnumSuggestionsMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
eventsIndexPattern,
|
||||
'test-field',
|
||||
'test-query',
|
||||
'test-filters',
|
||||
'test-field-meta',
|
||||
expect.any(Object)
|
||||
);
|
||||
|
||||
expect(mockResponse.ok).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should respond with bad request if wrong suggestion type', async () => {
|
||||
applyActionsEsSearchMock(
|
||||
mockScopedEsClient.asInternalUser,
|
||||
new EndpointActionGenerator().toEsSearchResponse([])
|
||||
);
|
||||
|
||||
const mockContext = requestContextMock.convertContext(
|
||||
createRouteHandlerContext(mockScopedEsClient, mockSavedObjectClient)
|
||||
);
|
||||
const mockRequest = httpServerMock.createKibanaRequest<
|
||||
TypeOf<typeof EndpointSuggestionsSchema.params>,
|
||||
never,
|
||||
never
|
||||
>({
|
||||
params: { suggestion_type: 'any' },
|
||||
});
|
||||
|
||||
await suggestionsRouteHandler(mockContext, mockRequest, mockResponse);
|
||||
|
||||
expect(mockResponse.badRequest).toHaveBeenCalledWith({
|
||||
body: 'Invalid suggestion_type: any',
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('without having right privileges', () => {
|
||||
beforeEach(() => {
|
||||
const startContract = createMockEndpointAppContextServiceStartContract();
|
||||
const routerMock = httpServiceMock.createRouter();
|
||||
const endpointAppContextService = new EndpointAppContextService();
|
||||
// add the suggestions route handlers to routerMock
|
||||
registerEndpointSuggestionsRoutes(routerMock, config$, {
|
||||
logFactory: loggingSystemMock.create(),
|
||||
service: endpointAppContextService,
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
});
|
||||
|
||||
// define a convenience function to execute an API call for a given route
|
||||
callRoute = async (
|
||||
routePrefix: string,
|
||||
{ params, authz = {} }: CallRouteInterface
|
||||
): Promise<void> => {
|
||||
const superUser = {
|
||||
username: 'superuser',
|
||||
roles: ['superuser'],
|
||||
};
|
||||
(startContract.security.authc.getCurrentUser as jest.Mock).mockImplementationOnce(
|
||||
() => superUser
|
||||
);
|
||||
|
||||
const ctx = createRouteHandlerContext(mockScopedEsClient, mockSavedObjectClient);
|
||||
|
||||
ctx.securitySolution.getEndpointAuthz.mockResolvedValue(
|
||||
getEndpointAuthzInitialStateMock(authz)
|
||||
);
|
||||
|
||||
const mockRequest = httpServerMock.createKibanaRequest({ params });
|
||||
const [, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
|
||||
path.startsWith(routePrefix)
|
||||
)!;
|
||||
|
||||
await routeHandler(ctx, mockRequest, mockResponse);
|
||||
};
|
||||
});
|
||||
|
||||
it('should respond with forbidden', async () => {
|
||||
await callRoute(SUGGESTIONS_ROUTE, {
|
||||
params: { suggestion_type: 'eventFilters' },
|
||||
authz: { canReadEventFilters: true, canWriteEventFilters: false },
|
||||
});
|
||||
|
||||
expect(mockResponse.forbidden).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 type { Observable } from 'rxjs';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import type { RequestHandler, Logger } from '@kbn/core/server';
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
import { getRequestAbortedSignal } from '@kbn/data-plugin/server';
|
||||
import type { ConfigSchema } from '@kbn/unified-search-plugin/config';
|
||||
import { termsEnumSuggestions } from '@kbn/unified-search-plugin/server/autocomplete/terms_enum';
|
||||
import {
|
||||
type EndpointSuggestionsBody,
|
||||
EndpointSuggestionsSchema,
|
||||
} from '../../../../common/endpoint/schema/suggestions';
|
||||
import type {
|
||||
SecuritySolutionPluginRouter,
|
||||
SecuritySolutionRequestHandlerContext,
|
||||
} from '../../../types';
|
||||
import type { EndpointAppContext } from '../../types';
|
||||
import { eventsIndexPattern, SUGGESTIONS_ROUTE } from '../../../../common/endpoint/constants';
|
||||
import { withEndpointAuthz } from '../with_endpoint_authz';
|
||||
import { errorHandler } from '../error_handler';
|
||||
|
||||
export const getLogger = (endpointAppContext: EndpointAppContext): Logger => {
|
||||
return endpointAppContext.logFactory.get('suggestions');
|
||||
};
|
||||
|
||||
export function registerEndpointSuggestionsRoutes(
|
||||
router: SecuritySolutionPluginRouter,
|
||||
config$: Observable<ConfigSchema>,
|
||||
endpointContext: EndpointAppContext
|
||||
) {
|
||||
router.post(
|
||||
{
|
||||
path: SUGGESTIONS_ROUTE,
|
||||
validate: EndpointSuggestionsSchema,
|
||||
},
|
||||
withEndpointAuthz(
|
||||
{ any: ['canWriteEventFilters'] },
|
||||
endpointContext.logFactory.get('endpointSuggestions'),
|
||||
getEndpointSuggestionsRequestHandler(config$, getLogger(endpointContext))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export const getEndpointSuggestionsRequestHandler = (
|
||||
config$: Observable<ConfigSchema>,
|
||||
logger: Logger
|
||||
): RequestHandler<
|
||||
TypeOf<typeof EndpointSuggestionsSchema.params>,
|
||||
never,
|
||||
EndpointSuggestionsBody,
|
||||
SecuritySolutionRequestHandlerContext
|
||||
> => {
|
||||
return async (context, request, response) => {
|
||||
const config = await firstValueFrom(config$);
|
||||
const { field: fieldName, query, filters, fieldMeta } = request.body;
|
||||
let index = '';
|
||||
|
||||
if (request.params.suggestion_type === 'eventFilters') {
|
||||
index = eventsIndexPattern;
|
||||
} else {
|
||||
return response.badRequest({
|
||||
body: `Invalid suggestion_type: ${request.params.suggestion_type}`,
|
||||
});
|
||||
}
|
||||
|
||||
const abortSignal = getRequestAbortedSignal(request.events.aborted$);
|
||||
const { savedObjects, elasticsearch } = await context.core;
|
||||
try {
|
||||
const body = await termsEnumSuggestions(
|
||||
config,
|
||||
savedObjects.client,
|
||||
elasticsearch.client.asInternalUser,
|
||||
index,
|
||||
fieldName,
|
||||
query,
|
||||
filters,
|
||||
fieldMeta,
|
||||
abortSignal
|
||||
);
|
||||
return response.ok({ body });
|
||||
} catch (error) {
|
||||
return errorHandler(logger, response, error);
|
||||
}
|
||||
};
|
||||
};
|
|
@ -56,6 +56,7 @@ import {
|
|||
import { registerEndpointRoutes } from './endpoint/routes/metadata';
|
||||
import { registerPolicyRoutes } from './endpoint/routes/policy';
|
||||
import { registerActionRoutes } from './endpoint/routes/actions';
|
||||
import { registerEndpointSuggestionsRoutes } from './endpoint/routes/suggestions';
|
||||
import { EndpointArtifactClient, ManifestManager } from './endpoint/services';
|
||||
import { EndpointAppContextService } from './endpoint/endpoint_app_context_services';
|
||||
import type { EndpointAppContext } from './endpoint/types';
|
||||
|
@ -102,6 +103,7 @@ import { EndpointFleetServicesFactory } from './endpoint/services/fleet';
|
|||
import { featureUsageService } from './endpoint/services/feature_usage';
|
||||
import { setIsElasticCloudDeployment } from './lib/telemetry/helpers';
|
||||
import { artifactService } from './lib/telemetry/artifact';
|
||||
import { eventFiltersFieldsProvider } from './search_strategy/event_filters_fields';
|
||||
|
||||
export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract';
|
||||
|
||||
|
@ -301,7 +303,13 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
previewRuleDataClient,
|
||||
this.telemetryReceiver
|
||||
);
|
||||
|
||||
registerEndpointRoutes(router, endpointContext);
|
||||
registerEndpointSuggestionsRoutes(
|
||||
router,
|
||||
plugins.unifiedSearch.autocomplete.getInitializerContextConfig().create(),
|
||||
endpointContext
|
||||
);
|
||||
registerLimitedConcurrencyRoutes(core);
|
||||
registerPolicyRoutes(router, endpointContext);
|
||||
registerActionRoutes(router, endpointContext);
|
||||
|
@ -349,6 +357,12 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
config,
|
||||
});
|
||||
|
||||
const eventFiltersFieldsStrategy = eventFiltersFieldsProvider(
|
||||
this.endpointAppContextService,
|
||||
depsStart.data.indexPatterns
|
||||
);
|
||||
plugins.data.search.registerSearchStrategy('eventFiltersFields', eventFiltersFieldsStrategy);
|
||||
|
||||
const securitySolutionSearchStrategy = securitySolutionSearchStrategyProvider(
|
||||
depsStart.data,
|
||||
endpointContext,
|
||||
|
|
|
@ -37,6 +37,7 @@ import type { TelemetryPluginStart, TelemetryPluginSetup } from '@kbn/telemetry-
|
|||
import type { OsqueryPluginSetup } from '@kbn/osquery-plugin/server';
|
||||
import type { CloudSetup } from '@kbn/cloud-plugin/server';
|
||||
import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common';
|
||||
import type { PluginSetup as UnifiedSearchServerPluginSetup } from '@kbn/unified-search-plugin/server';
|
||||
|
||||
export interface SecuritySolutionPluginSetupDependencies {
|
||||
alerting: AlertingPluginSetup;
|
||||
|
@ -55,6 +56,7 @@ export interface SecuritySolutionPluginSetupDependencies {
|
|||
usageCollection?: UsageCollectionPluginSetup;
|
||||
licensing: LicensingPluginSetup;
|
||||
osquery: OsqueryPluginSetup;
|
||||
unifiedSearch: UnifiedSearchServerPluginSetup;
|
||||
}
|
||||
|
||||
export interface SecuritySolutionPluginStartDependencies {
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* 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 type {
|
||||
SearchStrategyDependencies,
|
||||
DataViewsServerPluginStart,
|
||||
} from '@kbn/data-plugin/server';
|
||||
import { fieldsBeat as beatFields } from '@kbn/timelines-plugin/server/utils/beat_schema/fields';
|
||||
import { IndexPatternsFetcher } from '@kbn/data-plugin/server';
|
||||
import { requestEventFiltersFieldsSearch } from '.';
|
||||
import { createMockEndpointAppContextService } from '../../endpoint/mocks';
|
||||
import { getEndpointAuthzInitialStateMock } from '../../../common/endpoint/service/authz/mocks';
|
||||
import { eventsIndexPattern } from '../../../common/endpoint/constants';
|
||||
import { EndpointAuthorizationError } from '../../endpoint/errors';
|
||||
|
||||
describe('Event filters fields', () => {
|
||||
const getFieldsForWildcardMock = jest.fn();
|
||||
const esClientSearchMock = jest.fn();
|
||||
const esClientFieldCapsMock = jest.fn();
|
||||
const endpointAppContextService = createMockEndpointAppContextService();
|
||||
let IndexPatterns: DataViewsServerPluginStart;
|
||||
|
||||
const deps = {
|
||||
esClient: {
|
||||
asInternalUser: { search: esClientSearchMock, fieldCaps: esClientFieldCapsMock },
|
||||
},
|
||||
} as unknown as SearchStrategyDependencies;
|
||||
|
||||
const mockPattern = {
|
||||
title: 'test',
|
||||
fields: {
|
||||
toSpec: () => ({
|
||||
coolio: {
|
||||
name: 'name_test',
|
||||
type: 'type_test',
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
toSpec: () => ({
|
||||
runtimeFieldMap: { runtimeField: { type: 'keyword' } },
|
||||
}),
|
||||
};
|
||||
const getStartServices = jest.fn().mockReturnValue([
|
||||
null,
|
||||
{
|
||||
data: {
|
||||
indexPatterns: {
|
||||
dataViewsServiceFactory: () => ({
|
||||
get: jest.fn().mockReturnValue(mockPattern),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
beforeAll(() => {
|
||||
getFieldsForWildcardMock.mockResolvedValue([]);
|
||||
esClientSearchMock.mockResolvedValue({ hits: { total: { value: 123 } } });
|
||||
esClientFieldCapsMock.mockResolvedValue({ indices: ['value'] });
|
||||
IndexPatternsFetcher.prototype.getFieldsForWildcard = getFieldsForWildcardMock;
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const [
|
||||
,
|
||||
{
|
||||
data: { indexPatterns },
|
||||
},
|
||||
] = await getStartServices();
|
||||
IndexPatterns = indexPatterns;
|
||||
getFieldsForWildcardMock.mockClear();
|
||||
esClientSearchMock.mockClear();
|
||||
esClientFieldCapsMock.mockClear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
getFieldsForWildcardMock.mockRestore();
|
||||
});
|
||||
describe('with right privileges', () => {
|
||||
it('should check index exists', async () => {
|
||||
const indices = [eventsIndexPattern];
|
||||
const request = {
|
||||
indices,
|
||||
onlyCheckIfIndicesExist: true,
|
||||
};
|
||||
|
||||
const response = await requestEventFiltersFieldsSearch(
|
||||
endpointAppContextService,
|
||||
request,
|
||||
deps,
|
||||
beatFields,
|
||||
IndexPatterns
|
||||
);
|
||||
expect(response.indexFields).toHaveLength(0);
|
||||
expect(response.indicesExist).toEqual(indices);
|
||||
});
|
||||
|
||||
it('should search index fields', async () => {
|
||||
const indices = [eventsIndexPattern];
|
||||
const request = {
|
||||
indices,
|
||||
onlyCheckIfIndicesExist: false,
|
||||
};
|
||||
|
||||
const response = await requestEventFiltersFieldsSearch(
|
||||
endpointAppContextService,
|
||||
request,
|
||||
deps,
|
||||
beatFields,
|
||||
IndexPatterns
|
||||
);
|
||||
|
||||
expect(getFieldsForWildcardMock).toHaveBeenCalledWith({ pattern: indices[0] });
|
||||
|
||||
expect(response.indexFields).not.toHaveLength(0);
|
||||
expect(response.indicesExist).toEqual(indices);
|
||||
});
|
||||
|
||||
it('should throw when invalid index', async () => {
|
||||
const indices = ['invalid'];
|
||||
const request = {
|
||||
indices,
|
||||
onlyCheckIfIndicesExist: false,
|
||||
};
|
||||
|
||||
await expect(async () => {
|
||||
await requestEventFiltersFieldsSearch(
|
||||
endpointAppContextService,
|
||||
request,
|
||||
deps,
|
||||
beatFields,
|
||||
IndexPatterns
|
||||
);
|
||||
}).rejects.toThrowError('Invalid indices request invalid');
|
||||
});
|
||||
|
||||
it('should throw when more than one index', async () => {
|
||||
const indices = ['invalid', 'invalid2'];
|
||||
const request = {
|
||||
indices,
|
||||
onlyCheckIfIndicesExist: false,
|
||||
};
|
||||
|
||||
await expect(async () => {
|
||||
await requestEventFiltersFieldsSearch(
|
||||
endpointAppContextService,
|
||||
request,
|
||||
deps,
|
||||
beatFields,
|
||||
IndexPatterns
|
||||
);
|
||||
}).rejects.toThrowError('Invalid indices request invalid, invalid2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('without right privileges', () => {
|
||||
beforeEach(() => {
|
||||
(endpointAppContextService.getEndpointAuthz as jest.Mock).mockResolvedValue(
|
||||
getEndpointAuthzInitialStateMock({ canReadEventFilters: true, canWriteEventFilters: false })
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw because not enough privileges', async () => {
|
||||
const indices = [eventsIndexPattern];
|
||||
const request = {
|
||||
indices,
|
||||
onlyCheckIfIndicesExist: false,
|
||||
};
|
||||
|
||||
await expect(async () => {
|
||||
await requestEventFiltersFieldsSearch(
|
||||
endpointAppContextService,
|
||||
request,
|
||||
deps,
|
||||
beatFields,
|
||||
IndexPatterns
|
||||
);
|
||||
}).rejects.toThrowError(new EndpointAuthorizationError());
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { from } from 'rxjs';
|
||||
import type {
|
||||
DataViewsServerPluginStart,
|
||||
ISearchStrategy,
|
||||
SearchStrategyDependencies,
|
||||
} from '@kbn/data-plugin/server';
|
||||
|
||||
import { requestIndexFieldSearch } from '@kbn/timelines-plugin/server/search_strategy/index_fields';
|
||||
|
||||
import { eventsIndexPattern } from '../../../common/endpoint/constants';
|
||||
import type {
|
||||
BeatFields,
|
||||
IndexFieldsStrategyRequest,
|
||||
IndexFieldsStrategyResponse,
|
||||
} from '../../../common/search_strategy';
|
||||
import type { EndpointAppContextService } from '../../endpoint/endpoint_app_context_services';
|
||||
import { EndpointAuthorizationError } from '../../endpoint/errors';
|
||||
|
||||
/**
|
||||
* EventFiltersFieldProvider mimics indexField provider from timeline plugin: x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts
|
||||
* but it uses ES internalUser instead to avoid adding extra index privileges for users with event filters permissions.
|
||||
* It is used to retrieve index patterns for event filters form.
|
||||
*/
|
||||
export const eventFiltersFieldsProvider = (
|
||||
context: EndpointAppContextService,
|
||||
indexPatterns: DataViewsServerPluginStart
|
||||
): ISearchStrategy<IndexFieldsStrategyRequest<'indices'>, IndexFieldsStrategyResponse> => {
|
||||
// require the fields once we actually need them, rather than ahead of time, and pass
|
||||
// them to createFieldItem to reduce the amount of work done as much as possible
|
||||
const beatFields: BeatFields =
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require('@kbn/timelines-plugin/server/utils/beat_schema/fields').fieldsBeat;
|
||||
|
||||
return {
|
||||
search: (request, _, deps) =>
|
||||
from(requestEventFiltersFieldsSearch(context, request, deps, beatFields, indexPatterns)),
|
||||
};
|
||||
};
|
||||
|
||||
export const requestEventFiltersFieldsSearch = async (
|
||||
context: EndpointAppContextService,
|
||||
request: IndexFieldsStrategyRequest<'indices'>,
|
||||
deps: SearchStrategyDependencies,
|
||||
beatFields: BeatFields,
|
||||
indexPatterns: DataViewsServerPluginStart
|
||||
): Promise<IndexFieldsStrategyResponse> => {
|
||||
const { canWriteEventFilters } = await context.getEndpointAuthz(deps.request);
|
||||
|
||||
if (!canWriteEventFilters) {
|
||||
throw new EndpointAuthorizationError();
|
||||
}
|
||||
|
||||
if (request.indices.length > 1 || request.indices[0] !== eventsIndexPattern) {
|
||||
throw new Error(`Invalid indices request ${request.indices.join(', ')}`);
|
||||
}
|
||||
return requestIndexFieldSearch(request, deps, beatFields, indexPatterns, true);
|
||||
};
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { sortBy } from 'lodash/fp';
|
||||
|
||||
import { formatIndexFields, createFieldItem, requestIndexFieldSearch } from '.';
|
||||
import { formatIndexFields, createFieldItem, requestIndexFieldSearchHandler } from '.';
|
||||
import { mockAuditbeatIndexField, mockFilebeatIndexField, mockPacketbeatIndexField } from './mock';
|
||||
import { fieldsBeat as beatFields } from '../../utils/beat_schema/fields';
|
||||
import { IndexPatternsFetcher, SearchStrategyDependencies } from '@kbn/data-plugin/server';
|
||||
|
@ -231,146 +231,199 @@ describe('Fields Provider', () => {
|
|||
},
|
||||
]);
|
||||
|
||||
const deps = {
|
||||
const depsCurrentESUser = {
|
||||
esClient: { asCurrentUser: { search: esClientSearchMock, fieldCaps: esClientFieldCapsMock } },
|
||||
} as unknown as SearchStrategyDependencies;
|
||||
|
||||
beforeAll(() => {
|
||||
getFieldsForWildcardMock.mockResolvedValue([]);
|
||||
const depsInternalESUser = {
|
||||
esClient: {
|
||||
asInternalUser: { search: esClientSearchMock, fieldCaps: esClientFieldCapsMock },
|
||||
},
|
||||
} as unknown as SearchStrategyDependencies;
|
||||
|
||||
esClientSearchMock.mockResolvedValue({ hits: { total: { value: 123 } } });
|
||||
esClientFieldCapsMock.mockResolvedValue({ indices: ['value'] });
|
||||
IndexPatternsFetcher.prototype.getFieldsForWildcard = getFieldsForWildcardMock;
|
||||
});
|
||||
describe.each([
|
||||
['currentESUser', depsCurrentESUser, false],
|
||||
['internalESUser', depsInternalESUser, true],
|
||||
])(`Using %s`, (_, deps, useInternalUser) => {
|
||||
beforeAll(() => {
|
||||
getFieldsForWildcardMock.mockResolvedValue([]);
|
||||
|
||||
beforeEach(() => {
|
||||
getFieldsForWildcardMock.mockClear();
|
||||
esClientSearchMock.mockClear();
|
||||
esClientFieldCapsMock.mockClear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
getFieldsForWildcardMock.mockRestore();
|
||||
});
|
||||
|
||||
it('should check index exists', async () => {
|
||||
const indices = ['some-index-pattern-*'];
|
||||
const request = {
|
||||
indices,
|
||||
onlyCheckIfIndicesExist: true,
|
||||
};
|
||||
|
||||
const response = await requestIndexFieldSearch(request, deps, beatFields, getStartServices);
|
||||
expect(response.indexFields).toHaveLength(0);
|
||||
expect(response.indicesExist).toEqual(indices);
|
||||
});
|
||||
|
||||
it('should search index fields', async () => {
|
||||
const indices = ['some-index-pattern-*'];
|
||||
const request = {
|
||||
indices,
|
||||
onlyCheckIfIndicesExist: false,
|
||||
};
|
||||
|
||||
const response = await requestIndexFieldSearch(request, deps, beatFields, getStartServices);
|
||||
|
||||
expect(getFieldsForWildcardMock).toHaveBeenCalledWith({ pattern: indices[0] });
|
||||
|
||||
expect(response.indexFields).not.toHaveLength(0);
|
||||
expect(response.indicesExist).toEqual(indices);
|
||||
});
|
||||
|
||||
it('should search index fields by data view id', async () => {
|
||||
const dataViewId = 'id';
|
||||
const request = {
|
||||
dataViewId,
|
||||
onlyCheckIfIndicesExist: false,
|
||||
};
|
||||
|
||||
const response = await requestIndexFieldSearch(request, deps, beatFields, getStartServices);
|
||||
|
||||
expect(getFieldsForWildcardMock).not.toHaveBeenCalled();
|
||||
|
||||
expect(response.indexFields).not.toHaveLength(0);
|
||||
expect(response.indicesExist).toEqual(['coolbro']);
|
||||
});
|
||||
|
||||
it('onlyCheckIfIndicesExist by data view id', async () => {
|
||||
const dataViewId = 'id';
|
||||
const request = {
|
||||
dataViewId,
|
||||
onlyCheckIfIndicesExist: true,
|
||||
};
|
||||
|
||||
const response = await requestIndexFieldSearch(request, deps, beatFields, getStartServices);
|
||||
|
||||
expect(response.indexFields).toHaveLength(0);
|
||||
expect(response.indicesExist).toEqual(['coolbro']);
|
||||
});
|
||||
|
||||
it('should search apm index fields', async () => {
|
||||
const indices = ['apm-*-transaction*', 'traces-apm*'];
|
||||
const request = {
|
||||
indices,
|
||||
onlyCheckIfIndicesExist: false,
|
||||
};
|
||||
|
||||
const response = await requestIndexFieldSearch(request, deps, beatFields, getStartServices);
|
||||
|
||||
expect(getFieldsForWildcardMock).toHaveBeenCalledWith({ pattern: indices[0] });
|
||||
expect(response.indexFields).not.toHaveLength(0);
|
||||
expect(response.indicesExist).toEqual(indices);
|
||||
});
|
||||
|
||||
it('should check apm index exists with data', async () => {
|
||||
const indices = ['apm-*-transaction*', 'traces-apm*'];
|
||||
const request = {
|
||||
indices,
|
||||
onlyCheckIfIndicesExist: true,
|
||||
};
|
||||
|
||||
esClientSearchMock.mockResolvedValue({ hits: { total: { value: 1 } } });
|
||||
const response = await requestIndexFieldSearch(request, deps, beatFields, getStartServices);
|
||||
|
||||
expect(esClientSearchMock).toHaveBeenCalledWith({
|
||||
index: indices[0],
|
||||
body: { query: { match_all: {} }, size: 0 },
|
||||
});
|
||||
expect(esClientSearchMock).toHaveBeenCalledWith({
|
||||
index: indices[1],
|
||||
body: { query: { match_all: {} }, size: 0 },
|
||||
});
|
||||
expect(getFieldsForWildcardMock).not.toHaveBeenCalled();
|
||||
|
||||
expect(response.indexFields).toHaveLength(0);
|
||||
expect(response.indicesExist).toEqual(indices);
|
||||
});
|
||||
|
||||
it('should check apm index exists with no data', async () => {
|
||||
const indices = ['apm-*-transaction*', 'traces-apm*'];
|
||||
const request = {
|
||||
indices,
|
||||
onlyCheckIfIndicesExist: true,
|
||||
};
|
||||
|
||||
esClientSearchMock.mockResolvedValue({
|
||||
body: { hits: { total: { value: 0 } } },
|
||||
esClientSearchMock.mockResolvedValue({ hits: { total: { value: 123 } } });
|
||||
esClientFieldCapsMock.mockResolvedValue({ indices: ['value'] });
|
||||
IndexPatternsFetcher.prototype.getFieldsForWildcard = getFieldsForWildcardMock;
|
||||
});
|
||||
|
||||
const response = await requestIndexFieldSearch(request, deps, beatFields, getStartServices);
|
||||
|
||||
expect(esClientSearchMock).toHaveBeenCalledWith({
|
||||
index: indices[0],
|
||||
body: { query: { match_all: {} }, size: 0 },
|
||||
beforeEach(() => {
|
||||
getFieldsForWildcardMock.mockClear();
|
||||
esClientSearchMock.mockClear();
|
||||
esClientFieldCapsMock.mockClear();
|
||||
});
|
||||
expect(esClientSearchMock).toHaveBeenCalledWith({
|
||||
index: indices[1],
|
||||
body: { query: { match_all: {} }, size: 0 },
|
||||
});
|
||||
expect(getFieldsForWildcardMock).not.toHaveBeenCalled();
|
||||
|
||||
expect(response.indexFields).toHaveLength(0);
|
||||
expect(response.indicesExist).toEqual([]);
|
||||
afterAll(() => {
|
||||
getFieldsForWildcardMock.mockRestore();
|
||||
});
|
||||
|
||||
it('should check index exists', async () => {
|
||||
const indices = ['some-index-pattern-*'];
|
||||
const request = {
|
||||
indices,
|
||||
onlyCheckIfIndicesExist: true,
|
||||
};
|
||||
|
||||
const response = await requestIndexFieldSearchHandler(
|
||||
request,
|
||||
deps,
|
||||
beatFields,
|
||||
getStartServices,
|
||||
useInternalUser
|
||||
);
|
||||
expect(response.indexFields).toHaveLength(0);
|
||||
expect(response.indicesExist).toEqual(indices);
|
||||
});
|
||||
|
||||
it('should search index fields', async () => {
|
||||
const indices = ['some-index-pattern-*'];
|
||||
const request = {
|
||||
indices,
|
||||
onlyCheckIfIndicesExist: false,
|
||||
};
|
||||
|
||||
const response = await requestIndexFieldSearchHandler(
|
||||
request,
|
||||
deps,
|
||||
beatFields,
|
||||
getStartServices,
|
||||
useInternalUser
|
||||
);
|
||||
|
||||
expect(getFieldsForWildcardMock).toHaveBeenCalledWith({ pattern: indices[0] });
|
||||
|
||||
expect(response.indexFields).not.toHaveLength(0);
|
||||
expect(response.indicesExist).toEqual(indices);
|
||||
});
|
||||
|
||||
it('should search index fields by data view id', async () => {
|
||||
const dataViewId = 'id';
|
||||
const request = {
|
||||
dataViewId,
|
||||
onlyCheckIfIndicesExist: false,
|
||||
};
|
||||
|
||||
const response = await requestIndexFieldSearchHandler(
|
||||
request,
|
||||
deps,
|
||||
beatFields,
|
||||
getStartServices,
|
||||
useInternalUser
|
||||
);
|
||||
|
||||
expect(getFieldsForWildcardMock).not.toHaveBeenCalled();
|
||||
|
||||
expect(response.indexFields).not.toHaveLength(0);
|
||||
expect(response.indicesExist).toEqual(['coolbro']);
|
||||
});
|
||||
|
||||
it('onlyCheckIfIndicesExist by data view id', async () => {
|
||||
const dataViewId = 'id';
|
||||
const request = {
|
||||
dataViewId,
|
||||
onlyCheckIfIndicesExist: true,
|
||||
};
|
||||
|
||||
const response = await requestIndexFieldSearchHandler(
|
||||
request,
|
||||
deps,
|
||||
beatFields,
|
||||
getStartServices,
|
||||
useInternalUser
|
||||
);
|
||||
|
||||
expect(response.indexFields).toHaveLength(0);
|
||||
expect(response.indicesExist).toEqual(['coolbro']);
|
||||
});
|
||||
|
||||
it('should search apm index fields', async () => {
|
||||
const indices = ['apm-*-transaction*', 'traces-apm*'];
|
||||
const request = {
|
||||
indices,
|
||||
onlyCheckIfIndicesExist: false,
|
||||
};
|
||||
|
||||
const response = await requestIndexFieldSearchHandler(
|
||||
request,
|
||||
deps,
|
||||
beatFields,
|
||||
getStartServices,
|
||||
useInternalUser
|
||||
);
|
||||
|
||||
expect(getFieldsForWildcardMock).toHaveBeenCalledWith({ pattern: indices[0] });
|
||||
expect(response.indexFields).not.toHaveLength(0);
|
||||
expect(response.indicesExist).toEqual(indices);
|
||||
});
|
||||
|
||||
it('should check apm index exists with data', async () => {
|
||||
const indices = ['apm-*-transaction*', 'traces-apm*'];
|
||||
const request = {
|
||||
indices,
|
||||
onlyCheckIfIndicesExist: true,
|
||||
};
|
||||
|
||||
esClientSearchMock.mockResolvedValue({ hits: { total: { value: 1 } } });
|
||||
const response = await requestIndexFieldSearchHandler(
|
||||
request,
|
||||
deps,
|
||||
beatFields,
|
||||
getStartServices,
|
||||
useInternalUser
|
||||
);
|
||||
|
||||
expect(esClientSearchMock).toHaveBeenCalledWith({
|
||||
index: indices[0],
|
||||
body: { query: { match_all: {} }, size: 0 },
|
||||
});
|
||||
expect(esClientSearchMock).toHaveBeenCalledWith({
|
||||
index: indices[1],
|
||||
body: { query: { match_all: {} }, size: 0 },
|
||||
});
|
||||
expect(getFieldsForWildcardMock).not.toHaveBeenCalled();
|
||||
|
||||
expect(response.indexFields).toHaveLength(0);
|
||||
expect(response.indicesExist).toEqual(indices);
|
||||
});
|
||||
|
||||
it('should check apm index exists with no data', async () => {
|
||||
const indices = ['apm-*-transaction*', 'traces-apm*'];
|
||||
const request = {
|
||||
indices,
|
||||
onlyCheckIfIndicesExist: true,
|
||||
};
|
||||
|
||||
esClientSearchMock.mockResolvedValue({
|
||||
body: { hits: { total: { value: 0 } } },
|
||||
});
|
||||
|
||||
const response = await requestIndexFieldSearchHandler(
|
||||
request,
|
||||
deps,
|
||||
beatFields,
|
||||
getStartServices,
|
||||
useInternalUser
|
||||
);
|
||||
|
||||
expect(esClientSearchMock).toHaveBeenCalledWith({
|
||||
index: indices[0],
|
||||
body: { query: { match_all: {} }, size: 0 },
|
||||
});
|
||||
expect(esClientSearchMock).toHaveBeenCalledWith({
|
||||
index: indices[1],
|
||||
body: { query: { match_all: {} }, size: 0 },
|
||||
});
|
||||
expect(getFieldsForWildcardMock).not.toHaveBeenCalled();
|
||||
|
||||
expect(response.indexFields).toHaveLength(0);
|
||||
expect(response.indicesExist).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import isEmpty from 'lodash/isEmpty';
|
|||
import get from 'lodash/get';
|
||||
import { ElasticsearchClient, StartServicesAccessor } from '@kbn/core/server';
|
||||
import {
|
||||
DataViewsServerPluginStart,
|
||||
IndexPatternsFetcher,
|
||||
ISearchStrategy,
|
||||
SearchStrategyDependencies,
|
||||
|
@ -41,7 +42,7 @@ export const indexFieldsProvider = (
|
|||
|
||||
return {
|
||||
search: (request, options, deps) =>
|
||||
from(requestIndexFieldSearch(request, deps, beatFields, getStartServices)),
|
||||
from(requestIndexFieldSearchHandler(request, deps, beatFields, getStartServices)),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -70,27 +71,40 @@ export const findExistingIndices = async (
|
|||
.map((p) => p.catch((e) => false))
|
||||
);
|
||||
|
||||
export const requestIndexFieldSearch = async (
|
||||
export const requestIndexFieldSearchHandler = async (
|
||||
request: IndexFieldsStrategyRequest<'indices' | 'dataView'>,
|
||||
{ savedObjectsClient, esClient, request: kRequest }: SearchStrategyDependencies,
|
||||
deps: SearchStrategyDependencies,
|
||||
beatFields: BeatFields,
|
||||
getStartServices: StartServicesAccessor<StartPlugins>
|
||||
getStartServices: StartServicesAccessor<StartPlugins>,
|
||||
useInternalUser?: boolean
|
||||
): Promise<IndexFieldsStrategyResponse> => {
|
||||
const indexPatternsFetcherAsCurrentUser = new IndexPatternsFetcher(esClient.asCurrentUser);
|
||||
const indexPatternsFetcherAsInternalUser = new IndexPatternsFetcher(esClient.asInternalUser);
|
||||
if ('dataViewId' in request && 'indices' in request) {
|
||||
throw new Error('Provide index field search with either `dataViewId` or `indices`, not both');
|
||||
}
|
||||
const [
|
||||
,
|
||||
{
|
||||
data: { indexPatterns },
|
||||
},
|
||||
] = await getStartServices();
|
||||
return requestIndexFieldSearch(request, deps, beatFields, indexPatterns, useInternalUser);
|
||||
};
|
||||
|
||||
export const requestIndexFieldSearch = async (
|
||||
request: IndexFieldsStrategyRequest<'indices' | 'dataView'>,
|
||||
{ savedObjectsClient, esClient, request: kRequest }: SearchStrategyDependencies,
|
||||
beatFields: BeatFields,
|
||||
indexPatterns: DataViewsServerPluginStart,
|
||||
useInternalUser?: boolean
|
||||
): Promise<IndexFieldsStrategyResponse> => {
|
||||
const indexPatternsFetcherAsCurrentUser = new IndexPatternsFetcher(esClient.asCurrentUser);
|
||||
const indexPatternsFetcherAsInternalUser = new IndexPatternsFetcher(esClient.asInternalUser);
|
||||
if ('dataViewId' in request && 'indices' in request) {
|
||||
throw new Error('Provide index field search with either `dataViewId` or `indices`, not both');
|
||||
}
|
||||
|
||||
const esUser = useInternalUser ? esClient.asInternalUser : esClient.asCurrentUser;
|
||||
|
||||
const dataViewService = await indexPatterns.dataViewsServiceFactory(
|
||||
savedObjectsClient,
|
||||
esClient.asCurrentUser,
|
||||
esUser,
|
||||
kRequest,
|
||||
true
|
||||
);
|
||||
|
@ -118,7 +132,7 @@ export const requestIndexFieldSearch = async (
|
|||
}
|
||||
|
||||
const patternList = dataView.title.split(',');
|
||||
indicesExist = (await findExistingIndices(patternList, esClient.asCurrentUser)).reduce(
|
||||
indicesExist = (await findExistingIndices(patternList, esUser)).reduce(
|
||||
(acc: string[], doesIndexExist, i) => (doesIndexExist ? [...acc, patternList[i]] : acc),
|
||||
[]
|
||||
);
|
||||
|
@ -131,7 +145,7 @@ export const requestIndexFieldSearch = async (
|
|||
}
|
||||
} else if ('indices' in request) {
|
||||
const patternList = dedupeIndexName(request.indices);
|
||||
indicesExist = (await findExistingIndices(patternList, esClient.asCurrentUser)).reduce(
|
||||
indicesExist = (await findExistingIndices(patternList, esUser)).reduce(
|
||||
(acc: string[], doesIndexExist, i) => (doesIndexExist ? [...acc, patternList[i]] : acc),
|
||||
[]
|
||||
);
|
||||
|
@ -139,7 +153,7 @@ export const requestIndexFieldSearch = async (
|
|||
const fieldDescriptor = (
|
||||
await Promise.all(
|
||||
indicesExist.map(async (index, n) => {
|
||||
if (index.startsWith('.alerts-observability')) {
|
||||
if (index.startsWith('.alerts-observability') || useInternalUser) {
|
||||
return indexPatternsFetcherAsInternalUser.getFieldsForWildcard({
|
||||
pattern: index,
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue