mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
b3d7539475
commit
5c3f8b9941
47 changed files with 891 additions and 98 deletions
|
@ -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';
|
42
x-pack/plugins/lists/common/shared_exports.ts
Normal file
42
x-pack/plugins/lists/common/shared_exports.ts
Normal 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';
|
17
x-pack/plugins/lists/common/shared_imports.ts
Normal file
17
x-pack/plugins/lists/common/shared_imports.ts
Normal 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';
|
|
@ -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';
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
|
|
16
x-pack/plugins/lists/public/index.ts
Normal file
16
x-pack/plugins/lists/public/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* 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 };
|
|
@ -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"'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 }));
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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 }));
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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);
|
29
x-pack/plugins/lists/public/plugin.ts
Normal file
29
x-pack/plugins/lists/public/plugin.ts
Normal 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 {};
|
||||
}
|
||||
}
|
|
@ -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,
|
14
x-pack/plugins/lists/public/types.ts
Normal file
14
x-pack/plugins/lists/public/types.ts
Normal 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 {}
|
|
@ -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';
|
||||
|
|
|
@ -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({
|
||||
|
|
7
x-pack/plugins/security_solution/common/index.ts
Normal file
7
x-pack/plugins/security_solution/common/index.ts
Normal 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';
|
13
x-pack/plugins/security_solution/common/shared_exports.ts
Normal file
13
x-pack/plugins/security_solution/common/shared_exports.ts
Normal 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';
|
42
x-pack/plugins/security_solution/common/shared_imports.ts
Normal file
42
x-pack/plugins/security_solution/common/shared_imports.ts
Normal 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';
|
|
@ -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"')));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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(',')))
|
||||
);
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { has } from 'lodash/fp';
|
||||
|
||||
export interface KibanaApiError {
|
||||
name: string;
|
||||
message: string;
|
||||
body: {
|
||||
message: string;
|
||||
|
|
|
@ -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({});
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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 };
|
||||
};
|
|
@ -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 };
|
||||
};
|
|
@ -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 };
|
||||
};
|
|
@ -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');
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue