[Security Solution][Detections] Create value list indexes if they do not exist (#71360)

* Add API functions and hooks for reading and creating the lists index

* Ensure KibanaApiError extends the Error interface

It has a name, so we should type it as such. This way, we can use it
anywhere that an Error is accepted.

* Return an Error from validationEither and thus from our useAsync hooks

Because an io-ts pipeline needs a consistent type across its left value,
and validateEither was returning a string, we were forcing all our
errors to strings. In the case of an API error, however, this meant a
loss of data, since the original error's extra fields were lost.

By returning an Error from validateEither, we can now pass through Api
errors from useAsync and thus use them directly in kibana utilities like
toasts.addError.

* WIP: implements checking for and consequent creation of lists index

This adds most of the machinery that I think we're going to need. Not
featured here:

* lists privileges (stubbed out currently)
* handling when lists is disabled
* tests

* Add frontend plugin for lists

We need this to deteremine in security_solution whether lists is enabled
or not. There's no other functionality here, just boilerplate.

* Fix cross-plugin imports/exports

Now that lists has a client plugin, the optimizer cares about code
coming into and out of it.

By default, you cannot import another plugin's common/ folder into your
own common/ nor public/ folders. This is fixed by adding 'common' to
extraPublicDirs, however: extraPublicDirs need to resolve to modules.

Rather than adding each folder from which we export modules to
extraPublicDirs, I've added common/index.ts and exporting everything
through there.

By convention, I'm adding shared_exports.ts as an index of these exported modules,
and shared_imports.ts is used to import on the other end.

For now, I've left the ad hoc _deps files so as to limit the changes
here, but we should come back through and remove them at some point. NB
that I did remove lists_common_deps as it was only used in one or two
spots.

* Fix test failing due to lack of context

This component now uses useKibana indirectly through useListsConfig.

* Lists and securitySolution require each other's bundles

Without lists being a requiredBundle of securitySolution, we cannot
import its code when the plugin is disabled. The opposite is also true,
but there's no lists "app" to break.

* Fix logic in useListsConfig

Lists needs configuration if the index explicitly does not exist. If it
is true (already exists) or null (lists is disabled or we could not read
the index), we're good.

* useList* behavior when lists plugin is disabled

When the lists plugin is disabled, our calls in useListsIndex become no-ops so that:

* useListsIndex state does not change
* useListsConfig.needsConfiguration remains false as indexExists is
never non-null

This also removes use of our `useIsMounted` hook. Since the effects
we're consuming come from useAsync hooks, state will (already) not be
updated if the component is unmounted.

* Fix warning due to dynamic creation of a styled component

* Revert "Fix warning due to dynamic creation of a styled component"

This reverts commit 7124a8fbd9.

(This was already fixed on master)

* Check user's lists index privileges when determining configuration status

If there is no lists index and the user cannot create it, we will
display a configuration message in lieu of Detections

* Adds a lists hook to read privileges (missing schemae)
* Adds security hook useListsPrivileges to perform and parse the
privileges request
* Updates useListsConfig to use useListsPrivileges hook

* Move lists hooks to their own subfolder

* Redirect to main detections page if lists needs configuration

If:

* lists are enabled, and
* lists indexes DNE, and
* user cannot manage the lists indexes

Then they will be redirected to the main detections page where they'll
be instructed to configure detections. If any of the above is false,
things work as normal.

* Lock out of detections when user cannot write to value lists

Rather than add conditional logic to all our UI components dealing with
lists, we're going the heavy-handed route for now.

* Mock lists config hook in relevant Detections page tests

* Disable Detections when Lists is enabled

This refactors useListsConfig.needsConfiguration to mean:

* lists plugin is disabled, OR
* lists indexes DNE and can't be created, OR,
* user can't write to the lists index

In any of these situations, we want to disable detections, and so we
export that as a single boolean, needsConfiguration.

* Remove unneeded complexity exception

We refactored this to work 👍

* Remove outdated TODO

We link to our documentation, which will describe the lists aspects of
configuration.
This commit is contained in:
Ryland Herrick 2020-07-13 17:05:31 -05:00 committed by GitHub
parent b3d7539475
commit 5c3f8b9941
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 891 additions and 98 deletions

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { EntriesArray, exceptionListType, namespaceType } from '../../../lists/common/schemas';
export * from './shared_exports';

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export {
ListSchema,
CommentsArray,
CreateCommentsArray,
Comments,
CreateComments,
ExceptionListSchema,
ExceptionListItemSchema,
CreateExceptionListItemSchema,
UpdateExceptionListItemSchema,
Entry,
EntryExists,
EntryMatch,
EntryMatchAny,
EntryNested,
EntryList,
EntriesArray,
NamespaceType,
Operator,
OperatorEnum,
OperatorType,
OperatorTypeEnum,
ExceptionListTypeEnum,
exceptionListItemSchema,
exceptionListType,
createExceptionListItemSchema,
listSchema,
entry,
entriesNested,
entriesMatch,
entriesMatchAny,
entriesExists,
entriesList,
namespaceType,
ExceptionListType,
} from './schemas';

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export {
NonEmptyString,
DefaultUuid,
DefaultStringArray,
exactCheck,
getPaths,
foldLeftRight,
validate,
validateEither,
formatErrors,
} from '../../security_solution/common';

View file

@ -4,10 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { NonEmptyString } from '../../security_solution/common/detection_engine/schemas/types/non_empty_string';
export { DefaultUuid } from '../../security_solution/common/detection_engine/schemas/types/default_uuid';
export { DefaultStringArray } from '../../security_solution/common/detection_engine/schemas/types/default_string_array';
export { exactCheck } from '../../security_solution/common/exact_check';
export { getPaths, foldLeftRight } from '../../security_solution/common/test_utils';
export { validate, validateEither } from '../../security_solution/common/validate';
export { formatErrors } from '../../security_solution/common/format_errors';
// DEPRECATED: Do not add exports to this file; please import from shared_imports instead
export * from './shared_imports';

View file

@ -1,10 +1,12 @@
{
"configPath": ["xpack", "lists"],
"extraPublicDirs": ["common"],
"id": "lists",
"kibanaVersion": "kibana",
"requiredPlugins": [],
"optionalPlugins": ["spaces", "security"],
"requiredBundles": ["securitySolution"],
"server": true,
"ui": false,
"ui": true,
"version": "8.0.0"
}

View file

@ -16,3 +16,5 @@ export const toPromise = async <E, A>(taskEither: TaskEither<E, A>): Promise<A>
(a) => Promise.resolve(a)
)
);
export const toError = (e: unknown): Error => (e instanceof Error ? e : new Error(String(e)));

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './shared_exports';
import { PluginInitializerContext } from '../../../../src/core/public';
import { Plugin } from './plugin';
import { PluginSetup, PluginStart } from './types';
export const plugin = (context: PluginInitializerContext): Plugin => new Plugin(context);
export { Plugin, PluginSetup, PluginStart };

View file

@ -6,10 +6,19 @@
import { HttpFetchOptions } from '../../../../../src/core/public';
import { httpServiceMock } from '../../../../../src/core/public/mocks';
import { getAcknowledgeSchemaResponseMock } from '../../common/schemas/response/acknowledge_schema.mock';
import { getListResponseMock } from '../../common/schemas/response/list_schema.mock';
import { getListItemIndexExistSchemaResponseMock } from '../../common/schemas/response/list_item_index_exist_schema.mock';
import { getFoundListSchemaMock } from '../../common/schemas/response/found_list_schema.mock';
import { deleteList, exportList, findLists, importList } from './api';
import {
createListIndex,
deleteList,
exportList,
findLists,
importList,
readListIndex,
} from './api';
import {
ApiPayload,
DeleteListParams,
@ -60,7 +69,7 @@ describe('Value Lists API', () => {
...((payload as unknown) as ApiPayload<DeleteListParams>),
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "23" supplied to "id"');
).rejects.toEqual(new Error('Invalid value "23" supplied to "id"'));
expect(httpMock.fetch).not.toHaveBeenCalled();
});
@ -76,7 +85,7 @@ describe('Value Lists API', () => {
...payload,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "id"');
).rejects.toEqual(new Error('Invalid value "undefined" supplied to "id"'));
});
});
@ -129,7 +138,7 @@ describe('Value Lists API', () => {
...payload,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "0" supplied to "per_page"');
).rejects.toEqual(new Error('Invalid value "0" supplied to "per_page"'));
expect(httpMock.fetch).not.toHaveBeenCalled();
});
@ -145,7 +154,7 @@ describe('Value Lists API', () => {
...payload,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "cursor"');
).rejects.toEqual(new Error('Invalid value "undefined" supplied to "cursor"'));
});
});
@ -214,7 +223,7 @@ describe('Value Lists API', () => {
...payload,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "file"');
).rejects.toEqual(new Error('Invalid value "undefined" supplied to "file"'));
expect(httpMock.fetch).not.toHaveBeenCalled();
});
@ -233,7 +242,7 @@ describe('Value Lists API', () => {
...payload,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "other" supplied to "type"');
).rejects.toEqual(new Error('Invalid value "other" supplied to "type"'));
expect(httpMock.fetch).not.toHaveBeenCalled();
});
@ -254,7 +263,7 @@ describe('Value Lists API', () => {
...payload,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "id"');
).rejects.toEqual(new Error('Invalid value "undefined" supplied to "id"'));
});
});
@ -307,7 +316,7 @@ describe('Value Lists API', () => {
...payload,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "23" supplied to "list_id"');
).rejects.toEqual(new Error('Invalid value "23" supplied to "list_id"'));
expect(httpMock.fetch).not.toHaveBeenCalled();
});
@ -325,7 +334,95 @@ describe('Value Lists API', () => {
...payload,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "id"');
).rejects.toEqual(new Error('Invalid value "undefined" supplied to "id"'));
});
describe('readListIndex', () => {
beforeEach(() => {
httpMock.fetch.mockResolvedValue(getListItemIndexExistSchemaResponseMock());
});
it('GETs the list index', async () => {
const abortCtrl = new AbortController();
await readListIndex({
http: httpMock,
signal: abortCtrl.signal,
});
expect(httpMock.fetch).toHaveBeenCalledWith(
'/api/lists/index',
expect.objectContaining({
method: 'GET',
})
);
});
it('returns the response when valid', async () => {
const abortCtrl = new AbortController();
const result = await readListIndex({
http: httpMock,
signal: abortCtrl.signal,
});
expect(result).toEqual(getListItemIndexExistSchemaResponseMock());
});
it('rejects with an error if response payload is invalid', async () => {
const abortCtrl = new AbortController();
const badResponse = { ...getListItemIndexExistSchemaResponseMock(), list_index: undefined };
httpMock.fetch.mockResolvedValue(badResponse);
await expect(
readListIndex({
http: httpMock,
signal: abortCtrl.signal,
})
).rejects.toEqual(new Error('Invalid value "undefined" supplied to "list_index"'));
});
});
});
describe('createListIndex', () => {
beforeEach(() => {
httpMock.fetch.mockResolvedValue(getAcknowledgeSchemaResponseMock());
});
it('GETs the list index', async () => {
const abortCtrl = new AbortController();
await createListIndex({
http: httpMock,
signal: abortCtrl.signal,
});
expect(httpMock.fetch).toHaveBeenCalledWith(
'/api/lists/index',
expect.objectContaining({
method: 'POST',
})
);
});
it('returns the response when valid', async () => {
const abortCtrl = new AbortController();
const result = await createListIndex({
http: httpMock,
signal: abortCtrl.signal,
});
expect(result).toEqual(getAcknowledgeSchemaResponseMock());
});
it('rejects with an error if response payload is invalid', async () => {
const abortCtrl = new AbortController();
const badResponse = { acknowledged: undefined };
httpMock.fetch.mockResolvedValue(badResponse);
await expect(
createListIndex({
http: httpMock,
signal: abortCtrl.signal,
})
).rejects.toEqual(new Error('Invalid value "undefined" supplied to "acknowledged"'));
});
});
});

View file

@ -9,24 +9,28 @@ import { flow } from 'fp-ts/lib/function';
import { pipe } from 'fp-ts/lib/pipeable';
import {
AcknowledgeSchema,
DeleteListSchemaEncoded,
ExportListItemQuerySchemaEncoded,
FindListSchemaEncoded,
FoundListSchema,
ImportListItemQuerySchemaEncoded,
ImportListItemSchemaEncoded,
ListItemIndexExistSchema,
ListSchema,
acknowledgeSchema,
deleteListSchema,
exportListItemQuerySchema,
findListSchema,
foundListSchema,
importListItemQuerySchema,
importListItemSchema,
listItemIndexExistSchema,
listSchema,
} from '../../common/schemas';
import { LIST_ITEM_URL, LIST_URL } from '../../common/constants';
import { LIST_INDEX, LIST_ITEM_URL, LIST_PRIVILEGES_URL, LIST_URL } from '../../common/constants';
import { validateEither } from '../../common/siem_common_deps';
import { toPromise } from '../common/fp_utils';
import { toError, toPromise } from '../common/fp_utils';
import {
ApiParams,
@ -66,7 +70,7 @@ const findListsWithValidation = async ({
per_page: String(pageSize),
},
(payload) => fromEither(validateEither(findListSchema, payload)),
chain((payload) => tryCatch(() => findLists({ http, signal, ...payload }), String)),
chain((payload) => tryCatch(() => findLists({ http, signal, ...payload }), toError)),
chain((response) => fromEither(validateEither(foundListSchema, response))),
flow(toPromise)
);
@ -113,7 +117,7 @@ const importListWithValidation = async ({
map((body) => ({ ...body, ...query }))
)
),
chain((payload) => tryCatch(() => importList({ http, signal, ...payload }), String)),
chain((payload) => tryCatch(() => importList({ http, signal, ...payload }), toError)),
chain((response) => fromEither(validateEither(listSchema, response))),
flow(toPromise)
);
@ -139,7 +143,7 @@ const deleteListWithValidation = async ({
pipe(
{ id },
(payload) => fromEither(validateEither(deleteListSchema, payload)),
chain((payload) => tryCatch(() => deleteList({ http, signal, ...payload }), String)),
chain((payload) => tryCatch(() => deleteList({ http, signal, ...payload }), toError)),
chain((response) => fromEither(validateEither(listSchema, response))),
flow(toPromise)
);
@ -165,9 +169,52 @@ const exportListWithValidation = async ({
pipe(
{ list_id: listId },
(payload) => fromEither(validateEither(exportListItemQuerySchema, payload)),
chain((payload) => tryCatch(() => exportList({ http, signal, ...payload }), String)),
chain((payload) => tryCatch(() => exportList({ http, signal, ...payload }), toError)),
chain((response) => fromEither(validateEither(listSchema, response))),
flow(toPromise)
);
export { exportListWithValidation as exportList };
const readListIndex = async ({ http, signal }: ApiParams): Promise<ListItemIndexExistSchema> =>
http.fetch<ListItemIndexExistSchema>(LIST_INDEX, {
method: 'GET',
signal,
});
const readListIndexWithValidation = async ({
http,
signal,
}: ApiParams): Promise<ListItemIndexExistSchema> =>
flow(
() => tryCatch(() => readListIndex({ http, signal }), toError),
chain((response) => fromEither(validateEither(listItemIndexExistSchema, response))),
flow(toPromise)
)();
export { readListIndexWithValidation as readListIndex };
// TODO add types and validation
export const readListPrivileges = async ({ http, signal }: ApiParams): Promise<unknown> =>
http.fetch<unknown>(LIST_PRIVILEGES_URL, {
method: 'GET',
signal,
});
const createListIndex = async ({ http, signal }: ApiParams): Promise<AcknowledgeSchema> =>
http.fetch<AcknowledgeSchema>(LIST_INDEX, {
method: 'POST',
signal,
});
const createListIndexWithValidation = async ({
http,
signal,
}: ApiParams): Promise<AcknowledgeSchema> =>
flow(
() => tryCatch(() => createListIndex({ http, signal }), toError),
chain((response) => fromEither(validateEither(acknowledgeSchema, response))),
flow(toPromise)
)();
export { createListIndexWithValidation as createListIndex };

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import * as Api from '../api';
import { httpServiceMock } from '../../../../../../src/core/public/mocks';
import { getAcknowledgeSchemaResponseMock } from '../../../common/schemas/response/acknowledge_schema.mock';
import { useCreateListIndex } from './use_create_list_index';
jest.mock('../api');
describe('useCreateListIndex', () => {
let httpMock: ReturnType<typeof httpServiceMock.createStartContract>;
beforeEach(() => {
httpMock = httpServiceMock.createStartContract();
(Api.createListIndex as jest.Mock).mockResolvedValue(getAcknowledgeSchemaResponseMock());
});
it('invokes Api.createListIndex', async () => {
const { result, waitForNextUpdate } = renderHook(() => useCreateListIndex());
act(() => {
result.current.start({ http: httpMock });
});
await waitForNextUpdate();
expect(Api.createListIndex).toHaveBeenCalledWith(expect.objectContaining({ http: httpMock }));
});
});

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { withOptionalSignal } from '../../common/with_optional_signal';
import { useAsync } from '../../common/hooks/use_async';
import { createListIndex } from '../api';
const createListIndexWithOptionalSignal = withOptionalSignal(createListIndex);
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export const useCreateListIndex = () => useAsync(createListIndexWithOptionalSignal);

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import * as Api from '../api';
import { httpServiceMock } from '../../../../../../src/core/public/mocks';
import { getAcknowledgeSchemaResponseMock } from '../../../common/schemas/response/acknowledge_schema.mock';
import { useReadListIndex } from './use_read_list_index';
jest.mock('../api');
describe('useReadListIndex', () => {
let httpMock: ReturnType<typeof httpServiceMock.createStartContract>;
beforeEach(() => {
httpMock = httpServiceMock.createStartContract();
(Api.readListIndex as jest.Mock).mockResolvedValue(getAcknowledgeSchemaResponseMock());
});
it('invokes Api.readListIndex', async () => {
const { result, waitForNextUpdate } = renderHook(() => useReadListIndex());
act(() => {
result.current.start({ http: httpMock });
});
await waitForNextUpdate();
expect(Api.readListIndex).toHaveBeenCalledWith(expect.objectContaining({ http: httpMock }));
});
});

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { withOptionalSignal } from '../../common/with_optional_signal';
import { useAsync } from '../../common/hooks/use_async';
import { readListIndex } from '../api';
const readListIndexWithOptionalSignal = withOptionalSignal(readListIndex);
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export const useReadListIndex = () => useAsync(readListIndexWithOptionalSignal);

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { withOptionalSignal } from '../../common/with_optional_signal';
import { useAsync } from '../../common/hooks/use_async';
import { readListPrivileges } from '../api';
const readListPrivilegesWithOptionalSignal = withOptionalSignal(readListPrivileges);
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export const useReadListPrivileges = () => useAsync(readListPrivilegesWithOptionalSignal);

View file

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
CoreSetup,
CoreStart,
Plugin as IPlugin,
PluginInitializerContext,
} from '../../../../src/core/public';
import { PluginSetup, PluginStart, SetupPlugins, StartPlugins } from './types';
export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
constructor(initializerContext: PluginInitializerContext) {} // eslint-disable-line @typescript-eslint/no-useless-constructor
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public setup(core: CoreSetup<StartPlugins, PluginStart>, plugins: SetupPlugins): PluginSetup {
return {};
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public start(core: CoreStart, plugins: StartPlugins): PluginStart {
return {};
}
}

View file

@ -5,6 +5,7 @@
*/
// Exports to be shared with plugins
export { useIsMounted } from './common/hooks/use_is_mounted';
export { useApi } from './exceptions/hooks/use_api';
export { usePersistExceptionItem } from './exceptions/hooks/persist_exception_item';
export { usePersistExceptionList } from './exceptions/hooks/persist_exception_list';
@ -13,6 +14,9 @@ export { useFindLists } from './lists/hooks/use_find_lists';
export { useImportList } from './lists/hooks/use_import_list';
export { useDeleteList } from './lists/hooks/use_delete_list';
export { useExportList } from './lists/hooks/use_export_list';
export { useReadListIndex } from './lists/hooks/use_read_list_index';
export { useCreateListIndex } from './lists/hooks/use_create_list_index';
export { useReadListPrivileges } from './lists/hooks/use_read_list_privileges';
export {
addExceptionListItem,
updateExceptionListItem,

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PluginStart {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SetupPlugins {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface StartPlugins {}

View file

@ -17,7 +17,7 @@ import {
entriesMatch,
entriesNested,
ExceptionListItemSchema,
} from '../../../lists/common/schemas';
} from '../shared_imports';
import { Language, Query } from './schemas/common/schemas';
type Operators = 'and' | 'or' | 'not';

View file

@ -6,7 +6,7 @@
import * as t from 'io-ts';
import { exceptionListType, namespaceType } from '../../lists_common_deps';
import { exceptionListType, namespaceType } from '../../../shared_imports';
export const list = t.exact(
t.type({

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './shared_exports';

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { NonEmptyString } from './detection_engine/schemas/types/non_empty_string';
export { DefaultUuid } from './detection_engine/schemas/types/default_uuid';
export { DefaultStringArray } from './detection_engine/schemas/types/default_string_array';
export { exactCheck } from './exact_check';
export { getPaths, foldLeftRight } from './test_utils';
export { validate, validateEither } from './validate';
export { formatErrors } from './format_errors';

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export {
ListSchema,
CommentsArray,
CreateCommentsArray,
Comments,
CreateComments,
ExceptionListSchema,
ExceptionListItemSchema,
CreateExceptionListItemSchema,
UpdateExceptionListItemSchema,
Entry,
EntryExists,
EntryMatch,
EntryMatchAny,
EntryNested,
EntryList,
EntriesArray,
NamespaceType,
Operator,
OperatorEnum,
OperatorType,
OperatorTypeEnum,
ExceptionListTypeEnum,
exceptionListItemSchema,
exceptionListType,
createExceptionListItemSchema,
listSchema,
entry,
entriesNested,
entriesMatch,
entriesMatchAny,
entriesExists,
entriesList,
namespaceType,
ExceptionListType,
} from '../../lists/common';

View file

@ -43,6 +43,6 @@ describe('validateEither', () => {
const payload = { a: 'some other value' };
const result = validateEither(schema, payload);
expect(result).toEqual(left('Invalid value "some other value" supplied to "a"'));
expect(result).toEqual(left(new Error('Invalid value "some other value" supplied to "a"')));
});
});

View file

@ -27,9 +27,9 @@ export const validate = <T extends t.Mixed>(
export const validateEither = <T extends t.Mixed, A extends unknown>(
schema: T,
obj: A
): Either<string, A> =>
): Either<Error, A> =>
pipe(
obj,
(a) => schema.validate(a, t.getDefaultContext(schema.asDecoder())),
mapLeft((errors) => formatErrors(errors).join(','))
mapLeft((errors) => new Error(formatErrors(errors).join(',')))
);

View file

@ -1,6 +1,7 @@
{
"id": "securitySolution",
"version": "8.0.0",
"extraPublicDirs": ["common"],
"kibanaVersion": "kibana",
"configPath": ["xpack", "securitySolution"],
"requiredPlugins": [
@ -30,10 +31,5 @@
],
"server": true,
"ui": true,
"requiredBundles": [
"kibanaUtils",
"esUiShared",
"kibanaReact",
"ingestManager"
]
"requiredBundles": ["esUiShared", "ingestManager", "kibanaUtils", "kibanaReact", "lists"]
}

View file

@ -13,6 +13,7 @@ import { useUiSetting, useKibana } from './kibana_react';
import { errorToToaster, useStateToaster } from '../../components/toasters';
import { AuthenticatedUser } from '../../../../../security/common/model';
import { convertToCamelCase } from '../../../cases/containers/utils';
import { StartServices } from '../../../types';
export const useDateFormat = (): string => useUiSetting<string>(DEFAULT_DATE_FORMAT);
@ -124,3 +125,8 @@ export const useGetUserSavedObjectPermissions = () => {
return savedObjectsPermissions;
};
export const useToasts = (): StartServices['notifications']['toasts'] =>
useKibana().services.notifications.toasts;
export const useHttp = (): StartServices['http'] => useKibana().services.http;

View file

@ -7,6 +7,7 @@
import { has } from 'lodash/fp';
export interface KibanaApiError {
name: string;
message: string;
body: {
message: string;

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const useListsConfig = jest.fn().mockReturnValue({});

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const LISTS_INDEX_FETCH_FAILURE = i18n.translate(
'xpack.securitySolution.containers.detectionEngine.alerts.fetchListsIndex.errorDescription',
{
defaultMessage: 'Failed to retrieve the lists index',
}
);
export const LISTS_INDEX_CREATE_FAILURE = i18n.translate(
'xpack.securitySolution.containers.detectionEngine.alerts.createListsIndex.errorDescription',
{
defaultMessage: 'Failed to create the lists index',
}
);
export const LISTS_PRIVILEGES_READ_FAILURE = i18n.translate(
'xpack.securitySolution.containers.detectionEngine.alerts.readListsPrivileges.errorDescription',
{
defaultMessage: 'Failed to retrieve lists privileges',
}
);

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect } from 'react';
import { useKibana } from '../../../../common/lib/kibana';
import { useListsIndex } from './use_lists_index';
import { useListsPrivileges } from './use_lists_privileges';
export interface UseListsConfigReturn {
canManageIndex: boolean | null;
canWriteIndex: boolean | null;
enabled: boolean;
loading: boolean;
needsConfiguration: boolean;
}
export const useListsConfig = (): UseListsConfigReturn => {
const { createIndex, indexExists, loading: indexLoading } = useListsIndex();
const { canManageIndex, canWriteIndex, loading: privilegesLoading } = useListsPrivileges();
const { lists } = useKibana().services;
const enabled = lists != null;
const loading = indexLoading || privilegesLoading;
const needsIndex = indexExists === false;
const needsConfiguration = !enabled || needsIndex || canWriteIndex === false;
useEffect(() => {
if (canManageIndex && needsIndex) {
createIndex();
}
}, [canManageIndex, createIndex, needsIndex]);
return { canManageIndex, canWriteIndex, enabled, loading, needsConfiguration };
};

View file

@ -0,0 +1,100 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect, useState, useCallback } from 'react';
import { useReadListIndex, useCreateListIndex } from '../../../../shared_imports';
import { useHttp, useToasts, useKibana } from '../../../../common/lib/kibana';
import { isApiError } from '../../../../common/utils/api';
import * as i18n from './translations';
export interface UseListsIndexState {
indexExists: boolean | null;
}
export interface UseListsIndexReturn extends UseListsIndexState {
loading: boolean;
createIndex: () => void;
}
export const useListsIndex = (): UseListsIndexReturn => {
const [state, setState] = useState<UseListsIndexState>({
indexExists: null,
});
const { lists } = useKibana().services;
const http = useHttp();
const toasts = useToasts();
const { loading: readLoading, start: readListIndex, ...readListIndexState } = useReadListIndex();
const {
loading: createLoading,
start: createListIndex,
...createListIndexState
} = useCreateListIndex();
const loading = readLoading || createLoading;
const readIndex = useCallback(() => {
if (lists) {
readListIndex({ http });
}
}, [http, lists, readListIndex]);
const createIndex = useCallback(() => {
if (lists) {
createListIndex({ http });
}
}, [createListIndex, http, lists]);
// initial read list
useEffect(() => {
if (!readLoading && state.indexExists === null) {
readIndex();
}
}, [readIndex, readLoading, state.indexExists]);
// handle read result
useEffect(() => {
if (readListIndexState.result != null) {
setState({
indexExists:
readListIndexState.result.list_index && readListIndexState.result.list_item_index,
});
}
}, [readListIndexState.result]);
// refetch index after creation
useEffect(() => {
if (createListIndexState.result != null) {
readIndex();
}
}, [createListIndexState.result, readIndex]);
// handle read error
useEffect(() => {
const error = readListIndexState.error;
if (isApiError(error)) {
setState({ indexExists: false });
if (error.body.status_code !== 404) {
toasts.addError(error, {
title: i18n.LISTS_INDEX_FETCH_FAILURE,
toastMessage: error.body.message,
});
}
}
}, [readListIndexState.error, toasts]);
// handle create error
useEffect(() => {
const error = createListIndexState.error;
if (isApiError(error)) {
toasts.addError(error, {
title: i18n.LISTS_INDEX_CREATE_FAILURE,
toastMessage: error.body.message,
});
}
}, [createListIndexState.error, toasts]);
return { loading, createIndex, ...state };
};

View file

@ -0,0 +1,132 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect, useState, useCallback } from 'react';
import { useReadListPrivileges } from '../../../../shared_imports';
import { useHttp, useToasts, useKibana } from '../../../../common/lib/kibana';
import { isApiError } from '../../../../common/utils/api';
import * as i18n from './translations';
export interface UseListsPrivilegesState {
isAuthenticated: boolean | null;
canManageIndex: boolean | null;
canWriteIndex: boolean | null;
}
export interface UseListsPrivilegesReturn extends UseListsPrivilegesState {
loading: boolean;
}
interface ListIndexPrivileges {
[indexName: string]: {
all: boolean;
create: boolean;
create_doc: boolean;
create_index: boolean;
delete: boolean;
delete_index: boolean;
index: boolean;
manage: boolean;
manage_follow_index: boolean;
manage_ilm: boolean;
manage_leader_index: boolean;
monitor: boolean;
read: boolean;
read_cross_cluster: boolean;
view_index_metadata: boolean;
write: boolean;
};
}
interface ListPrivileges {
is_authenticated: boolean;
lists: {
index: ListIndexPrivileges;
};
listItems: {
index: ListIndexPrivileges;
};
}
const canManageIndex = (indexPrivileges: ListIndexPrivileges): boolean => {
const [indexName] = Object.keys(indexPrivileges);
const privileges = indexPrivileges[indexName];
if (privileges == null) {
return false;
}
return privileges.manage;
};
const canWriteIndex = (indexPrivileges: ListIndexPrivileges): boolean => {
const [indexName] = Object.keys(indexPrivileges);
const privileges = indexPrivileges[indexName];
if (privileges == null) {
return false;
}
return privileges.create || privileges.create_doc || privileges.index || privileges.write;
};
export const useListsPrivileges = (): UseListsPrivilegesReturn => {
const [state, setState] = useState<UseListsPrivilegesState>({
isAuthenticated: null,
canManageIndex: null,
canWriteIndex: null,
});
const { lists } = useKibana().services;
const http = useHttp();
const toasts = useToasts();
const { loading, start: readListPrivileges, ...privilegesState } = useReadListPrivileges();
const readPrivileges = useCallback(() => {
if (lists) {
readListPrivileges({ http });
}
}, [http, lists, readListPrivileges]);
// initRead
useEffect(() => {
if (!loading && state.isAuthenticated === null) {
readPrivileges();
}
}, [loading, readPrivileges, state.isAuthenticated]);
// handleReadResult
useEffect(() => {
if (privilegesState.result != null) {
try {
const {
is_authenticated: isAuthenticated,
lists: { index: listsPrivileges },
listItems: { index: listItemsPrivileges },
} = privilegesState.result as ListPrivileges;
setState({
isAuthenticated,
canManageIndex: canManageIndex(listsPrivileges) && canManageIndex(listItemsPrivileges),
canWriteIndex: canWriteIndex(listsPrivileges) && canWriteIndex(listItemsPrivileges),
});
} catch (e) {
setState({ isAuthenticated: null, canManageIndex: false, canWriteIndex: false });
}
}
}, [privilegesState.result]);
// handleReadError
useEffect(() => {
const error = privilegesState.error;
if (isApiError(error)) {
setState({ isAuthenticated: null, canManageIndex: false, canWriteIndex: false });
toasts.addError(error, {
title: i18n.LISTS_PRIVILEGES_READ_FAILURE,
toastMessage: error.body.message,
});
}
}, [privilegesState.error, toasts]);
return { loading, ...state };
};

View file

@ -14,6 +14,7 @@ import { DetectionEnginePageComponent } from './detection_engine';
import { useUserInfo } from '../../components/user_info';
import { useWithSource } from '../../../common/containers/source';
jest.mock('../../containers/detection_engine/lists/use_lists_config');
jest.mock('../../components/user_info');
jest.mock('../../../common/containers/source');
jest.mock('../../../common/components/link_to');

View file

@ -34,6 +34,7 @@ import { useUserInfo } from '../../components/user_info';
import { OverviewEmpty } from '../../../overview/components/overview_empty';
import { DetectionEngineNoIndex } from './detection_engine_no_signal_index';
import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page';
import { useListsConfig } from '../../containers/detection_engine/lists/use_lists_config';
import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated';
import * as i18n from './translations';
import { LinkButton } from '../../../common/components/links';
@ -46,7 +47,7 @@ export const DetectionEnginePageComponent: React.FC<PropsFromRedux> = ({
}) => {
const { to, from, deleteQuery, setQuery } = useGlobalTime();
const {
loading,
loading: userInfoLoading,
isSignalIndexExists,
isAuthenticated: isUserAuthenticated,
hasEncryptionKey,
@ -54,9 +55,14 @@ export const DetectionEnginePageComponent: React.FC<PropsFromRedux> = ({
signalIndexName,
hasIndexWrite,
} = useUserInfo();
const {
loading: listsConfigLoading,
needsConfiguration: needsListsConfiguration,
} = useListsConfig();
const history = useHistory();
const [lastAlerts] = useAlertInfo({});
const { formatUrl } = useFormatUrl(SecurityPageName.detections);
const loading = userInfoLoading || listsConfigLoading;
const updateDateRangeCallback = useCallback<UpdateDateRange>(
({ x }) => {
@ -90,7 +96,8 @@ export const DetectionEnginePageComponent: React.FC<PropsFromRedux> = ({
</WrapperPage>
);
}
if (isSignalIndexExists != null && !isSignalIndexExists && !loading) {
if (!loading && (isSignalIndexExists === false || needsListsConfiguration)) {
return (
<WrapperPage>
<DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} />

View file

@ -22,6 +22,7 @@ jest.mock('react-router-dom', () => {
};
});
jest.mock('../../../../containers/detection_engine/lists/use_lists_config');
jest.mock('../../../../../common/components/link_to');
jest.mock('../../../../components/user_info');

View file

@ -10,6 +10,7 @@ import { useHistory } from 'react-router-dom';
import styled, { StyledComponent } from 'styled-components';
import { usePersistRule } from '../../../../containers/detection_engine/rules';
import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config';
import {
getRulesUrl,
@ -84,12 +85,17 @@ StepDefineRuleAccordion.displayName = 'StepDefineRuleAccordion';
const CreateRulePageComponent: React.FC = () => {
const {
loading,
loading: userInfoLoading,
isSignalIndexExists,
isAuthenticated,
hasEncryptionKey,
canUserCRUD,
} = useUserInfo();
const {
loading: listsConfigLoading,
needsConfiguration: needsListsConfiguration,
} = useListsConfig();
const loading = userInfoLoading || listsConfigLoading;
const [, dispatchToaster] = useStateToaster();
const [openAccordionId, setOpenAccordionId] = useState<RuleStep>(RuleStep.defineRule);
const defineRuleRef = useRef<EuiAccordion | null>(null);
@ -278,7 +284,14 @@ const CreateRulePageComponent: React.FC = () => {
return null;
}
if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) {
if (
redirectToDetections(
isSignalIndexExists,
isAuthenticated,
hasEncryptionKey,
needsListsConfiguration
)
) {
history.replace(getDetectionEngineUrl());
return null;
} else if (userHasNoPermissions(canUserCRUD)) {

View file

@ -15,6 +15,7 @@ import { useUserInfo } from '../../../../components/user_info';
import { useWithSource } from '../../../../../common/containers/source';
import { useParams } from 'react-router-dom';
jest.mock('../../../../containers/detection_engine/lists/use_lists_config');
jest.mock('../../../../../common/components/link_to');
jest.mock('../../../../components/user_info');
jest.mock('../../../../../common/containers/source');

View file

@ -34,6 +34,7 @@ import {
import { SiemSearchBar } from '../../../../../common/components/search_bar';
import { WrapperPage } from '../../../../../common/components/wrapper_page';
import { useRule } from '../../../../containers/detection_engine/rules';
import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config';
import { useWithSource } from '../../../../../common/containers/source';
import { SpyRoute } from '../../../../../common/utils/route/spy_routes';
@ -105,7 +106,7 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({
}) => {
const { to, from, deleteQuery, setQuery } = useGlobalTime();
const {
loading,
loading: userInfoLoading,
isSignalIndexExists,
isAuthenticated,
hasEncryptionKey,
@ -113,6 +114,11 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({
hasIndexWrite,
signalIndexName,
} = useUserInfo();
const {
loading: listsConfigLoading,
needsConfiguration: needsListsConfiguration,
} = useListsConfig();
const loading = userInfoLoading || listsConfigLoading;
const { detailName: ruleId } = useParams();
const [isLoading, rule] = useRule(ruleId);
// This is used to re-trigger api rule status when user de/activate rule
@ -282,7 +288,14 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({
}
}, [rule]);
if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) {
if (
redirectToDetections(
isSignalIndexExists,
isAuthenticated,
hasEncryptionKey,
needsListsConfiguration
)
) {
history.replace(getDetectionEngineUrl());
return null;
}

View file

@ -12,6 +12,7 @@ import { EditRulePage } from './index';
import { useUserInfo } from '../../../../components/user_info';
import { useParams } from 'react-router-dom';
jest.mock('../../../../containers/detection_engine/lists/use_lists_config');
jest.mock('../../../../../common/components/link_to');
jest.mock('../../../../components/user_info');
jest.mock('react-router-dom', () => {

View file

@ -20,6 +20,7 @@ import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } fr
import { useParams, useHistory } from 'react-router-dom';
import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules';
import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config';
import { WrapperPage } from '../../../../../common/components/wrapper_page';
import {
getRuleDetailsUrl,
@ -74,12 +75,17 @@ const EditRulePageComponent: FC = () => {
const history = useHistory();
const [, dispatchToaster] = useStateToaster();
const {
loading: initLoading,
loading: userInfoLoading,
isSignalIndexExists,
isAuthenticated,
hasEncryptionKey,
canUserCRUD,
} = useUserInfo();
const {
loading: listsConfigLoading,
needsConfiguration: needsListsConfiguration,
} = useListsConfig();
const initLoading = userInfoLoading || listsConfigLoading;
const { detailName: ruleId } = useParams();
const [loading, rule] = useRule(ruleId);
@ -365,7 +371,14 @@ const EditRulePageComponent: FC = () => {
return null;
}
if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) {
if (
redirectToDetections(
isSignalIndexExists,
isAuthenticated,
hasEncryptionKey,
needsListsConfiguration
)
) {
history.replace(getDetectionEngineUrl());
return null;
} else if (userHasNoPermissions(canUserCRUD)) {

View file

@ -236,12 +236,13 @@ export const setFieldValue = (
export const redirectToDetections = (
isSignalIndexExists: boolean | null,
isAuthenticated: boolean | null,
hasEncryptionKey: boolean | null
hasEncryptionKey: boolean | null,
needsListsConfiguration: boolean
) =>
isSignalIndexExists != null &&
isAuthenticated != null &&
hasEncryptionKey != null &&
(!isSignalIndexExists || !isAuthenticated || !hasEncryptionKey);
isSignalIndexExists === false ||
isAuthenticated === false ||
hasEncryptionKey === false ||
needsListsConfiguration;
export const getActionMessageRuleParams = (ruleType: RuleType): string[] => {
const commonRuleParamsKeys = [

View file

@ -22,6 +22,7 @@ jest.mock('react-router-dom', () => {
};
});
jest.mock('../../../containers/detection_engine/lists/use_lists_config');
jest.mock('../../../../common/components/link_to');
jest.mock('../../../components/user_info');
jest.mock('../../../containers/detection_engine/rules');

View file

@ -9,6 +9,7 @@ import React, { useCallback, useRef, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { usePrePackagedRules, importRules } from '../../../containers/detection_engine/rules';
import { useListsConfig } from '../../../containers/detection_engine/lists/use_lists_config';
import {
getDetectionEngineUrl,
getCreateRuleUrl,
@ -35,13 +36,18 @@ const RulesPageComponent: React.FC = () => {
const [showImportModal, setShowImportModal] = useState(false);
const refreshRulesData = useRef<null | Func>(null);
const {
loading,
loading: userInfoLoading,
isSignalIndexExists,
isAuthenticated,
hasEncryptionKey,
canUserCRUD,
hasIndexWrite,
} = useUserInfo();
const {
loading: listsConfigLoading,
needsConfiguration: needsListsConfiguration,
} = useListsConfig();
const loading = userInfoLoading || listsConfigLoading;
const {
createPrePackagedRules,
loading: prePackagedRuleLoading,
@ -58,12 +64,12 @@ const RulesPageComponent: React.FC = () => {
isAuthenticated,
hasEncryptionKey,
});
const { formatUrl } = useFormatUrl(SecurityPageName.detections);
const prePackagedRuleStatus = getPrePackagedRuleStatus(
rulesInstalled,
rulesNotInstalled,
rulesNotUpdated
);
const { formatUrl } = useFormatUrl(SecurityPageName.detections);
const handleRefreshRules = useCallback(async () => {
if (refreshRulesData.current != null) {
@ -96,7 +102,14 @@ const RulesPageComponent: React.FC = () => {
[history]
);
if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) {
if (
redirectToDetections(
isSignalIndexExists,
isAuthenticated,
hasEncryptionKey,
needsListsConfiguration
)
) {
history.replace(getDetectionEngineUrl());
return null;
}

View file

@ -4,48 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
export {
useApi,
useExceptionList,
usePersistExceptionItem,
usePersistExceptionList,
useFindLists,
addExceptionListItem,
updateExceptionListItem,
fetchExceptionListById,
addExceptionList,
ExceptionIdentifiers,
ExceptionList,
Pagination,
UseExceptionListSuccess,
} from '../../lists/public';
export {
ListSchema,
CommentsArray,
CreateCommentsArray,
Comments,
CreateComments,
ExceptionListSchema,
ExceptionListItemSchema,
CreateExceptionListItemSchema,
UpdateExceptionListItemSchema,
Entry,
EntryExists,
EntryNested,
EntryList,
EntriesArray,
NamespaceType,
Operator,
OperatorEnum,
OperatorType,
OperatorTypeEnum,
ExceptionListTypeEnum,
exceptionListItemSchema,
createExceptionListItemSchema,
listSchema,
entry,
entriesNested,
entriesExists,
entriesList,
ExceptionListType,
} from '../../lists/common/schemas';
// DEPRECATED: Do not add exports to this file; please import from shared_imports instead
export * from './shared_imports';

View file

@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
export * from '../common/shared_imports';
export {
getUseField,
getFieldValidityAndErrorMessage,
@ -23,3 +25,23 @@ export {
export { Field, SelectField } from '../../../../src/plugins/es_ui_shared/static/forms/components';
export { fieldValidators } from '../../../../src/plugins/es_ui_shared/static/forms/helpers';
export { ERROR_CODE } from '../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types';
export {
useIsMounted,
useApi,
useExceptionList,
usePersistExceptionItem,
usePersistExceptionList,
useFindLists,
useCreateListIndex,
useReadListIndex,
useReadListPrivileges,
addExceptionListItem,
updateExceptionListItem,
fetchExceptionListById,
addExceptionList,
ExceptionIdentifiers,
ExceptionList,
Pagination,
UseExceptionListSuccess,
} from '../../lists/public';

View file

@ -14,6 +14,7 @@ import { UiActionsStart } from '../../../../src/plugins/ui_actions/public';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
import { Storage } from '../../../../src/plugins/kibana_utils/public';
import { IngestManagerStart } from '../../ingest_manager/public';
import { PluginStart as ListsPluginStart } from '../../lists/public';
import {
TriggersAndActionsUIPublicPluginSetup as TriggersActionsSetup,
TriggersAndActionsUIPublicPluginStart as TriggersActionsStart,
@ -33,6 +34,7 @@ export interface StartPlugins {
embeddable: EmbeddableStart;
inspector: InspectorStart;
ingestManager?: IngestManagerStart;
lists?: ListsPluginStart;
newsfeed?: NewsfeedStart;
triggers_actions_ui: TriggersActionsStart;
uiActions: UiActionsStart;

View file

@ -9,7 +9,7 @@ import sinon from 'sinon';
import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks';
import { listMock } from '../../../../../lists/server/mocks';
import { EntriesArray } from '../../../../common/detection_engine/lists_common_deps';
import { EntriesArray } from '../../../../common/shared_imports';
import { buildRuleMessageFactory } from './rule_messages';
import { ExceptionListClient } from '../../../../../lists/server';
import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock';