Skip to content

Commit

Permalink
[Security Solution][Detections] - Add skeleton exceptions list tab to…
Browse files Browse the repository at this point in the history
… all rules page (#85465) (#86079)

## Summary

This PR is the first of 2 to complete the addition of a table displaying all exception lists on the all rules page. This PR focuses on the following:

- all exception lists displayed
- 'number of rules assigned to' displayed
- names and links of rules assigned to displayed
- refresh action button working
- no trusted apps list show
- search by `name`, `created_by`, `list_id`
  - just searching a word will search by list name
  - to search by `created_by` type `created_by:ytercero`
  - to search by `list_id` type `list_id:some-list-id`

#### TO DO (follow up PR)
- [ ] add tests
- [ ] wire up export of exception list
- [ ] wire up deletion of exception list

<img width="1121" alt="Screen Shot 2020-12-09 at 2 10 59 PM" src="https://user-images.githubusercontent.com/10927944/101676548-50498e00-3a29-11eb-90cb-5f56fc8c0a1b.png">

### Checklist
- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)
- [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials
- [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

Co-authored-by: Yara Tercero <[email protected]>
  • Loading branch information
spong and yctercero authored Dec 16, 2020
1 parent 4f5f2f5 commit 27ad160
Show file tree
Hide file tree
Showing 37 changed files with 2,267 additions and 612 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ pageLoadAssetSize:
licenseManagement: 41961
lensOss: 19341
licensing: 39008
lists: 183665
lists: 202261
logstash: 53548
management: 46112
maps: 183754
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const getFindExceptionListSchemaMock = (): FindExceptionListSchema => ({

export const getFindExceptionListSchemaDecodedMock = (): FindExceptionListSchemaDecoded => ({
filter: FILTER,
namespace_type: NAMESPACE_TYPE,
namespace_type: [NAMESPACE_TYPE],
page: 1,
per_page: 25,
sort_field: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('find_exception_list_schema', () => {
expect(getPaths(left(message.errors))).toEqual([]);
const expected: FindExceptionListSchemaDecoded = {
filter: undefined,
namespace_type: 'single',
namespace_type: ['single'],
page: undefined,
per_page: undefined,
sort_field: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@

import * as t from 'io-ts';

import { filter, namespace_type, sort_field, sort_order } from '../common/schemas';
import { filter, sort_field, sort_order } from '../common/schemas';
import { RequiredKeepUndefined } from '../../types';
import { StringToPositiveNumber } from '../types/string_to_positive_number';
import { NamespaceType } from '../types';
import { DefaultNamespaceArray, NamespaceTypeArray } from '../types/default_namespace_array';

export const findExceptionListSchema = t.exact(
t.partial({
filter, // defaults to undefined if not set during decode
namespace_type, // defaults to 'single' if not set during decode
namespace_type: DefaultNamespaceArray, // defaults to 'single' if not set during decode
page: StringToPositiveNumber, // defaults to undefined if not set during decode
per_page: StringToPositiveNumber, // defaults to undefined if not set during decode
sort_field, // defaults to undefined if not set during decode
Expand All @@ -29,5 +29,5 @@ export type FindExceptionListSchemaDecoded = Omit<
RequiredKeepUndefined<t.TypeOf<typeof findExceptionListSchema>>,
'namespace_type'
> & {
namespace_type: NamespaceType;
namespace_type: NamespaceTypeArray;
};
4 changes: 4 additions & 0 deletions x-pack/plugins/lists/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/

export const exceptionListSavedObjectType = 'exception-list';
export const exceptionListAgnosticSavedObjectType = 'exception-list-agnostic';
export type SavedObjectType = 'exception-list' | 'exception-list-agnostic';

/**
* This makes any optional property the same as Required<T> would but also has the
* added benefit of keeping your undefined.
Expand Down
85 changes: 84 additions & 1 deletion x-pack/plugins/lists/public/exceptions/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ExceptionListItemSchema,
ExceptionListSchema,
} from '../../common/schemas';
import { getFoundExceptionListSchemaMock } from '../../common/schemas/response/found_exception_list_schema.mock';

import {
addEndpointExceptionList,
Expand All @@ -26,11 +27,12 @@ import {
deleteExceptionListItemById,
fetchExceptionListById,
fetchExceptionListItemById,
fetchExceptionLists,
fetchExceptionListsItemsByListIds,
updateExceptionList,
updateExceptionListItem,
} from './api';
import { ApiCallByIdProps, ApiCallByListIdProps } from './types';
import { ApiCallByIdProps, ApiCallByListIdProps, ApiCallFetchExceptionListsProps } from './types';

const abortCtrl = new AbortController();

Expand Down Expand Up @@ -289,6 +291,87 @@ describe('Exceptions Lists API', () => {
});
});

describe('#fetchExceptionLists', () => {
beforeEach(() => {
httpMock.fetch.mockResolvedValue(getFoundExceptionListSchemaMock());
});

test('it invokes "fetchExceptionLists" with expected url and body values', async () => {
await fetchExceptionLists({
filters: 'exception-list.attributes.name: Sample Endpoint',
http: httpMock,
namespaceTypes: 'single,agnostic',
pagination: {
page: 1,
perPage: 20,
},
signal: abortCtrl.signal,
});
expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/_find', {
method: 'GET',
query: {
filter: 'exception-list.attributes.name: Sample Endpoint',
namespace_type: 'single,agnostic',
page: '1',
per_page: '20',
sort_field: 'exception-list.created_at',
sort_order: 'desc',
},
signal: abortCtrl.signal,
});
});

test('it returns expected exception list on success', async () => {
const exceptionResponse = await fetchExceptionLists({
filters: 'exception-list.attributes.name: Sample Endpoint',
http: httpMock,
namespaceTypes: 'single,agnostic',
pagination: {
page: 1,
perPage: 20,
},
signal: abortCtrl.signal,
});
expect(exceptionResponse.data).toEqual([getExceptionListSchemaMock()]);
});

test('it returns error and does not make request if request payload fails decode', async () => {
const payload = ({
filters: 'exception-list.attributes.name: Sample Endpoint',
http: httpMock,
namespaceTypes: 'notANamespaceType',
pagination: {
page: 1,
perPage: 20,
},
signal: abortCtrl.signal,
} as unknown) as ApiCallFetchExceptionListsProps & { namespaceTypes: string[] };
await expect(fetchExceptionLists(payload)).rejects.toEqual(
'Invalid value "notANamespaceType" supplied to "namespace_type"'
);
});

test('it returns error if response payload fails decode', async () => {
const badPayload = getExceptionListSchemaMock();
// @ts-expect-error
delete badPayload.id;
httpMock.fetch.mockResolvedValue({ data: [badPayload], page: 1, per_page: 20, total: 1 });

await expect(
fetchExceptionLists({
filters: 'exception-list.attributes.name: Sample Endpoint',
http: httpMock,
namespaceTypes: 'single,agnostic',
pagination: {
page: 1,
perPage: 20,
},
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "data,id"');
});
});

describe('#fetchExceptionListById', () => {
beforeEach(() => {
httpMock.fetch.mockResolvedValue(getExceptionListSchemaMock());
Expand Down
56 changes: 56 additions & 0 deletions x-pack/plugins/lists/public/exceptions/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ExceptionListItemSchema,
ExceptionListSchema,
FoundExceptionListItemSchema,
FoundExceptionListSchema,
createEndpointListSchema,
createExceptionListItemSchema,
createExceptionListSchema,
Expand All @@ -23,7 +24,9 @@ import {
exceptionListItemSchema,
exceptionListSchema,
findExceptionListItemSchema,
findExceptionListSchema,
foundExceptionListItemSchema,
foundExceptionListSchema,
readExceptionListItemSchema,
readExceptionListSchema,
updateExceptionListItemSchema,
Expand All @@ -37,6 +40,7 @@ import {
AddExceptionListProps,
ApiCallByIdProps,
ApiCallByListIdProps,
ApiCallFetchExceptionListsProps,
UpdateExceptionListItemProps,
UpdateExceptionListProps,
} from './types';
Expand Down Expand Up @@ -201,6 +205,58 @@ export const updateExceptionListItem = async ({
}
};

/**
* Fetch all ExceptionLists (optionally by namespaceType)
*
* @param http Kibana http service
* @param namespaceTypes ExceptionList namespace_types of lists to find
* @param filters search bar filters
* @param pagination optional
* @param signal to cancel request
*
* @throws An error if request params or response is not OK
*/
export const fetchExceptionLists = async ({
http,
filters,
namespaceTypes,
pagination,
signal,
}: ApiCallFetchExceptionListsProps): Promise<FoundExceptionListSchema> => {
const query = {
filter: filters,
namespace_type: namespaceTypes,
page: pagination.page ? `${pagination.page}` : '1',
per_page: pagination.perPage ? `${pagination.perPage}` : '20',
sort_field: 'exception-list.created_at',
sort_order: 'desc',
};

const [validatedRequest, errorsRequest] = validate(query, findExceptionListSchema);

if (validatedRequest != null) {
try {
const response = await http.fetch<ExceptionListSchema>(`${EXCEPTION_LIST_URL}/_find`, {
method: 'GET',
query,
signal,
});

const [validatedResponse, errorsResponse] = validate(response, foundExceptionListSchema);

if (errorsResponse != null || validatedResponse == null) {
return Promise.reject(errorsResponse);
} else {
return Promise.resolve(validatedResponse);
}
} catch (error) {
return Promise.reject(error);
}
} else {
return Promise.reject(errorsRequest);
}
};

/**
* Fetch an ExceptionList by providing a ExceptionList ID
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import { coreMock } from '../../../../../../src/core/public/mocks';
import * as api from '../api';
import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock';
import { ExceptionListItemSchema } from '../../../common/schemas';
import { UseExceptionListProps, UseExceptionListSuccess } from '../types';
import { UseExceptionListItemsSuccess, UseExceptionListProps } from '../types';

import { ReturnExceptionListAndItems, useExceptionList } from './use_exception_list';
import { ReturnExceptionListAndItems, useExceptionListItems } from './use_exception_list_items';

const mockKibanaHttpService = coreMock.createStart().http;

describe('useExceptionList', () => {
describe('useExceptionListItems', () => {
const onErrorMock = jest.fn();

beforeEach(() => {
Expand All @@ -36,7 +36,7 @@ describe('useExceptionList', () => {
UseExceptionListProps,
ReturnExceptionListAndItems
>(() =>
useExceptionList({
useExceptionListItems({
filterOptions: [],
http: mockKibanaHttpService,
lists: [
Expand Down Expand Up @@ -75,7 +75,7 @@ describe('useExceptionList', () => {
UseExceptionListProps,
ReturnExceptionListAndItems
>(() =>
useExceptionList({
useExceptionListItems({
filterOptions: [],
http: mockKibanaHttpService,
lists: [
Expand All @@ -100,7 +100,7 @@ describe('useExceptionList', () => {

const expectedListItemsResult: ExceptionListItemSchema[] = getFoundExceptionListItemSchemaMock()
.data;
const expectedResult: UseExceptionListSuccess = {
const expectedResult: UseExceptionListItemsSuccess = {
exceptions: expectedListItemsResult,
pagination: { page: 1, perPage: 1, total: 1 },
};
Expand Down Expand Up @@ -129,7 +129,7 @@ describe('useExceptionList', () => {
const onSuccessMock = jest.fn();
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
() =>
useExceptionList({
useExceptionListItems({
filterOptions: [],
http: mockKibanaHttpService,
lists: [
Expand Down Expand Up @@ -179,7 +179,7 @@ describe('useExceptionList', () => {
const onSuccessMock = jest.fn();
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
() =>
useExceptionList({
useExceptionListItems({
filterOptions: [],
http: mockKibanaHttpService,
lists: [
Expand Down Expand Up @@ -231,7 +231,7 @@ describe('useExceptionList', () => {
UseExceptionListProps,
ReturnExceptionListAndItems
>(() =>
useExceptionList({
useExceptionListItems({
filterOptions: [],
http: mockKibanaHttpService,
lists: [
Expand Down Expand Up @@ -278,7 +278,7 @@ describe('useExceptionList', () => {
const onSuccessMock = jest.fn();
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
() =>
useExceptionList({
useExceptionListItems({
filterOptions: [{ filter: 'host.name', tags: [] }],
http: mockKibanaHttpService,
lists: [
Expand Down Expand Up @@ -343,7 +343,7 @@ describe('useExceptionList', () => {
showDetectionsListsOnly,
showEndpointListsOnly,
}) =>
useExceptionList({
useExceptionListItems({
filterOptions,
http,
lists,
Expand Down Expand Up @@ -413,7 +413,7 @@ describe('useExceptionList', () => {
UseExceptionListProps,
ReturnExceptionListAndItems
>(() =>
useExceptionList({
useExceptionListItems({
filterOptions: [],
http: mockKibanaHttpService,
lists: [
Expand Down Expand Up @@ -455,7 +455,7 @@ describe('useExceptionList', () => {
await act(async () => {
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
() =>
useExceptionList({
useExceptionListItems({
filterOptions: [],
http: mockKibanaHttpService,
lists: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export type ReturnExceptionListAndItems = [
* @param pagination optional
*
*/
export const useExceptionList = ({
export const useExceptionListItems = ({
http,
lists,
pagination = {
Expand Down
Loading

0 comments on commit 27ad160

Please sign in to comment.