Skip to content

Commit

Permalink
[Security solution][Endpoint] Uses new search strategy in endpoints l…
Browse files Browse the repository at this point in the history
…ist (#146852)

## Summary

- Uses new search strategy in endpoints list.
- Uses endpoints list RBAC privileges in search strategy.
- Renamed search strategy to be more generic.
- Adds test cases.

### 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)
  • Loading branch information
dasansol92 authored Dec 2, 2022
1 parent 9ad5772 commit 9b68601
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 33 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/security_solution/common/endpoint/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,5 @@ export const ENDPOINT_DEFAULT_PAGE_SIZE = 10;
export const ENDPOINT_ERROR_CODES: Record<string, number> = {
ES_CONNECTION_ERROR: -272,
};

export const ENDPOINT_FIELDS_SEARCH_STRATEGY = 'endpointFields';
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import { firstValueFrom } from 'rxjs';
import type { CoreStart, HttpSetup } from '@kbn/core/public';
import type { Store } from 'redux';
import { applyMiddleware, createStore } from 'redux';
Expand Down Expand Up @@ -56,11 +57,13 @@ jest.mock('../../../services/policies/ingest', () => ({
}));

jest.mock('../../../../common/lib/kibana');
jest.mock('rxjs');

type EndpointListStore = Store<Immutable<EndpointState>, Immutable<AppAction>>;

describe('endpoint list middleware', () => {
const getKibanaServicesMock = KibanaServices.get as jest.Mock;
const firstValueFromMock = firstValueFrom as jest.Mock;
let fakeCoreStart: jest.Mocked<CoreStart>;
let depsStart: DepsStartMock;
let fakeHttpServices: jest.Mocked<HttpSetup>;
Expand Down Expand Up @@ -120,6 +123,7 @@ describe('endpoint list middleware', () => {
});

it('handles `appRequestedEndpointList`', async () => {
firstValueFromMock.mockResolvedValue({ indexFields: [] });
endpointPageHttpMock(fakeHttpServices);
const apiResponse = getEndpointListApiResponse();
fakeHttpServices.get.mockResolvedValue(apiResponse);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@
* 2.0.
*/

import { firstValueFrom } from 'rxjs';
import type { DataViewBase, Query } from '@kbn/es-query';
import type { CoreStart, HttpStart } from '@kbn/core/public';
import type { Dispatch } from 'redux';
import semverGte from 'semver/functions/gte';
import type {
IndexFieldsStrategyRequest,
IndexFieldsStrategyResponse,
} from '@kbn/timelines-plugin/common';
import {
BASE_POLICY_RESPONSE_ROUTE,
HOST_METADATA_GET_ROUTE,
HOST_METADATA_LIST_ROUTE,
metadataCurrentIndexPattern,
METADATA_UNITED_INDEX,
METADATA_TRANSFORMS_STATUS_ROUTE,
ENDPOINT_FIELDS_SEARCH_STRATEGY,
} from '../../../../../common/endpoint/constants';
import type {
GetHostPolicyResponse,
Expand Down Expand Up @@ -94,13 +100,19 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory<EndpointState
? METADATA_UNITED_INDEX
: metadataCurrentIndexPattern;

const { indexPatterns } = depsStart.data;
const fields = await indexPatterns.getFieldsForWildcard({
pattern: indexPatternToFetch,
});
const res$ = depsStart.data.search.search<
IndexFieldsStrategyRequest<'indices'>,
IndexFieldsStrategyResponse
>(
{ indices: [indexPatternToFetch], onlyCheckIfIndicesExist: false },
{
strategy: ENDPOINT_FIELDS_SEARCH_STRATEGY,
}
);
const response = await firstValueFrom(res$);
const indexPattern: DataViewBase = {
title: indexPatternToFetch,
fields,
fields: response.indexFields,
};
return [indexPattern];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ import { fireEvent } from '@testing-library/dom';
import { uiQueryParams } from '../../../store/selectors';
import type { EndpointIndexUIQueryParams } from '../../../types';

jest.mock('rxjs', () => {
const actual = jest.requireActual('rxjs');
return {
...actual,
firstValueFrom: async () => ({ indexFields: [] }),
};
});

describe('when rendering the endpoint list `AdminSearchBar`', () => {
let render: (
urlParams?: EndpointIndexUIQueryParams
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ 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 {
ENDPOINT_FIELDS_SEARCH_STRATEGY,
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';
Expand Down Expand Up @@ -146,7 +149,7 @@ export const EventFiltersForm: React.FC<ArtifactFormComponentProps & { allowSele
const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(
indexNames,
undefined,
'eventFiltersFields'
ENDPOINT_FIELDS_SEARCH_STRATEGY
);

const [areConditionsValid, setAreConditionsValid] = useState(
Expand Down
10 changes: 7 additions & 3 deletions x-pack/plugins/security_solution/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ 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';
import { endpointFieldsProvider } from './search_strategy/endpoint_fields';
import { ENDPOINT_FIELDS_SEARCH_STRATEGY } from '../common/endpoint/constants';

export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract';

Expand Down Expand Up @@ -357,11 +358,14 @@ export class Plugin implements ISecuritySolutionPlugin {
config,
});

const eventFiltersFieldsStrategy = eventFiltersFieldsProvider(
const endpointFieldsStrategy = endpointFieldsProvider(
this.endpointAppContextService,
depsStart.data.indexPatterns
);
plugins.data.search.registerSearchStrategy('eventFiltersFields', eventFiltersFieldsStrategy);
plugins.data.search.registerSearchStrategy(
ENDPOINT_FIELDS_SEARCH_STRATEGY,
endpointFieldsStrategy
);

const securitySolutionSearchStrategy = securitySolutionSearchStrategyProvider(
depsStart.data,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import type {
} 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 { requestEndpointFieldsSearch } from '.';
import { createMockEndpointAppContextService } from '../../endpoint/mocks';
import { getEndpointAuthzInitialStateMock } from '../../../common/endpoint/service/authz/mocks';
import { eventsIndexPattern } from '../../../common/endpoint/constants';
import { eventsIndexPattern, METADATA_UNITED_INDEX } from '../../../common/endpoint/constants';
import { EndpointAuthorizationError } from '../../endpoint/errors';

describe('Event filters fields', () => {
describe('Endpoint fields', () => {
const getFieldsForWildcardMock = jest.fn();
const esClientSearchMock = jest.fn();
const esClientFieldCapsMock = jest.fn();
Expand Down Expand Up @@ -83,14 +83,14 @@ describe('Event filters fields', () => {
getFieldsForWildcardMock.mockRestore();
});
describe('with right privileges', () => {
it('should check index exists', async () => {
it('should check index exists for event filters', async () => {
const indices = [eventsIndexPattern];
const request = {
indices,
onlyCheckIfIndicesExist: true,
};

const response = await requestEventFiltersFieldsSearch(
const response = await requestEndpointFieldsSearch(
endpointAppContextService,
request,
deps,
Expand All @@ -101,14 +101,53 @@ describe('Event filters fields', () => {
expect(response.indicesExist).toEqual(indices);
});

it('should search index fields', async () => {
it('should check index exists for endpoints list', async () => {
const indices = [METADATA_UNITED_INDEX];
const request = {
indices,
onlyCheckIfIndicesExist: true,
};

const response = await requestEndpointFieldsSearch(
endpointAppContextService,
request,
deps,
beatFields,
IndexPatterns
);
expect(response.indexFields).toHaveLength(0);
expect(response.indicesExist).toEqual(indices);
});

it('should search index fields for event filters', async () => {
const indices = [eventsIndexPattern];
const request = {
indices,
onlyCheckIfIndicesExist: false,
};

const response = await requestEventFiltersFieldsSearch(
const response = await requestEndpointFieldsSearch(
endpointAppContextService,
request,
deps,
beatFields,
IndexPatterns
);

expect(getFieldsForWildcardMock).toHaveBeenCalledWith({ pattern: indices[0] });

expect(response.indexFields).not.toHaveLength(0);
expect(response.indicesExist).toEqual(indices);
});

it('should search index fields for endpoints list', async () => {
const indices = [METADATA_UNITED_INDEX];
const request = {
indices,
onlyCheckIfIndicesExist: false,
};

const response = await requestEndpointFieldsSearch(
endpointAppContextService,
request,
deps,
Expand All @@ -130,7 +169,7 @@ describe('Event filters fields', () => {
};

await expect(async () => {
await requestEventFiltersFieldsSearch(
await requestEndpointFieldsSearch(
endpointAppContextService,
request,
deps,
Expand All @@ -148,7 +187,7 @@ describe('Event filters fields', () => {
};

await expect(async () => {
await requestEventFiltersFieldsSearch(
await requestEndpointFieldsSearch(
endpointAppContextService,
request,
deps,
Expand All @@ -160,21 +199,42 @@ describe('Event filters fields', () => {
});

describe('without right privileges', () => {
beforeEach(() => {
it('should throw because not enough privileges for event filters', async () => {
(endpointAppContextService.getEndpointAuthz as jest.Mock).mockResolvedValue(
getEndpointAuthzInitialStateMock({ canReadEventFilters: true, canWriteEventFilters: false })
);
const indices = [eventsIndexPattern];
const request = {
indices,
onlyCheckIfIndicesExist: false,
};

await expect(async () => {
await requestEndpointFieldsSearch(
endpointAppContextService,
request,
deps,
beatFields,
IndexPatterns
);
}).rejects.toThrowError(new EndpointAuthorizationError());
});

it('should throw because not enough privileges', async () => {
const indices = [eventsIndexPattern];
it('should throw because not enough privileges for endpoints list', async () => {
(endpointAppContextService.getEndpointAuthz as jest.Mock).mockResolvedValue(
getEndpointAuthzInitialStateMock({
canReadEndpointList: false,
canWriteEndpointList: false,
})
);
const indices = [METADATA_UNITED_INDEX];
const request = {
indices,
onlyCheckIfIndicesExist: false,
};

await expect(async () => {
await requestEventFiltersFieldsSearch(
await requestEndpointFieldsSearch(
endpointAppContextService,
request,
deps,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {

import { requestIndexFieldSearch } from '@kbn/timelines-plugin/server/search_strategy/index_fields';

import { eventsIndexPattern } from '../../../common/endpoint/constants';
import { eventsIndexPattern, METADATA_UNITED_INDEX } from '../../../common/endpoint/constants';
import type {
BeatFields,
IndexFieldsStrategyRequest,
Expand All @@ -24,11 +24,11 @@ import type { EndpointAppContextService } from '../../endpoint/endpoint_app_cont
import { EndpointAuthorizationError } from '../../endpoint/errors';

/**
* EventFiltersFieldProvider mimics indexField provider from timeline plugin: x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts
* EndpointFieldProvider 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 = (
export const endpointFieldsProvider = (
context: EndpointAppContextService,
indexPatterns: DataViewsServerPluginStart
): ISearchStrategy<IndexFieldsStrategyRequest<'indices'>, IndexFieldsStrategyResponse> => {
Expand All @@ -40,25 +40,34 @@ export const eventFiltersFieldsProvider = (

return {
search: (request, _, deps) =>
from(requestEventFiltersFieldsSearch(context, request, deps, beatFields, indexPatterns)),
from(requestEndpointFieldsSearch(context, request, deps, beatFields, indexPatterns)),
};
};

export const requestEventFiltersFieldsSearch = async (
export const requestEndpointFieldsSearch = async (
context: EndpointAppContextService,
request: IndexFieldsStrategyRequest<'indices'>,
deps: SearchStrategyDependencies,
beatFields: BeatFields,
indexPatterns: DataViewsServerPluginStart
): Promise<IndexFieldsStrategyResponse> => {
const { canWriteEventFilters } = await context.getEndpointAuthz(deps.request);
if (
request.indices.length > 1 ||
(request.indices[0] !== eventsIndexPattern && request.indices[0] !== METADATA_UNITED_INDEX)
) {
throw new Error(`Invalid indices request ${request.indices.join(', ')}`);
}

const { canWriteEventFilters, canReadEndpointList } = await context.getEndpointAuthz(
deps.request
);

if (!canWriteEventFilters) {
if (
(!canWriteEventFilters && request.indices[0] === eventsIndexPattern) ||
(!canReadEndpointList && request.indices[0] === METADATA_UNITED_INDEX)
) {
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);
};

0 comments on commit 9b68601

Please sign in to comment.