Skip to content

Commit

Permalink
[Security Solution] Add host isolation exception IPs UI (#113762)
Browse files Browse the repository at this point in the history
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
academo and kibanamachine authored Oct 11, 2021
1 parent f2bfa59 commit 2ffbf6e
Show file tree
Hide file tree
Showing 17 changed files with 925 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import {
CreateExceptionListItemSchema,
ExceptionListItemSchema,
FoundExceptionListItemSchema,
} from '@kbn/securitysolution-io-ts-list-types';
Expand Down Expand Up @@ -65,6 +66,19 @@ export async function getHostIsolationExceptionItems({
return entries;
}

export async function createHostIsolationExceptionItem({
http,
exception,
}: {
http: HttpStart;
exception: CreateExceptionListItemSchema;
}): Promise<ExceptionListItemSchema> {
await ensureHostIsolationExceptionsListExists(http);
return http.post<ExceptionListItemSchema>(EXCEPTION_LIST_ITEM_URL, {
body: JSON.stringify(exception),
});
}

export async function deleteHostIsolationExceptionItems(http: HttpStart, id: string) {
await ensureHostIsolationExceptionsListExists(http);
return http.delete<ExceptionListItemSchema>(EXCEPTION_LIST_ITEM_URL, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ export type HostIsolationExceptionsPageDataChanged =
payload: HostIsolationExceptionsPageState['entries'];
};

export type HostIsolationExceptionsFormStateChanged =
Action<'hostIsolationExceptionsFormStateChanged'> & {
payload: HostIsolationExceptionsPageState['form']['status'];
};

export type HostIsolationExceptionsFormEntryChanged =
Action<'hostIsolationExceptionsFormEntryChanged'> & {
payload: HostIsolationExceptionsPageState['form']['entry'];
};

export type HostIsolationExceptionsCreateEntry = Action<'hostIsolationExceptionsCreateEntry'> & {
payload: HostIsolationExceptionsPageState['form']['entry'];
};

export type HostIsolationExceptionsDeleteItem = Action<'hostIsolationExceptionsMarkToDelete'> & {
payload?: ExceptionListItemSchema;
};
Expand All @@ -24,9 +38,10 @@ export type HostIsolationExceptionsDeleteStatusChanged =
Action<'hostIsolationExceptionsDeleteStatusChanged'> & {
payload: HostIsolationExceptionsPageState['deletion']['status'];
};

export type HostIsolationExceptionsPageAction =
| HostIsolationExceptionsPageDataChanged
| HostIsolationExceptionsCreateEntry
| HostIsolationExceptionsFormStateChanged
| HostIsolationExceptionsDeleteItem
| HostIsolationExceptionsSubmitDelete
| HostIsolationExceptionsDeleteStatusChanged;
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export const initialHostIsolationExceptionsPageState = (): HostIsolationExceptio
page_size: MANAGEMENT_DEFAULT_PAGE_SIZE,
filter: '',
},
form: {
entry: undefined,
status: createUninitialisedResourceState(),
},
deletion: {
item: undefined,
status: createUninitialisedResourceState(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
* 2.0.
*/

import { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { applyMiddleware, createStore, Store } from 'redux';
import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../common/constants';
import { coreMock } from '../../../../../../../../src/core/public/mocks';
import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock';
import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../common/constants';
import { AppAction } from '../../../../common/store/actions';
import {
createSpyMiddleware,
Expand All @@ -19,8 +20,13 @@ import {
isLoadedResourceState,
isLoadingResourceState,
} from '../../../state';
import { getHostIsolationExceptionItems, deleteHostIsolationExceptionItems } from '../service';
import {
createHostIsolationExceptionItem,
deleteHostIsolationExceptionItems,
getHostIsolationExceptionItems,
} from '../service';
import { HostIsolationExceptionsPageState } from '../types';
import { createEmptyHostIsolationException } from '../utils';
import { initialHostIsolationExceptionsPageState } from './builders';
import { createHostIsolationExceptionsPageMiddleware } from './middleware';
import { hostIsolationExceptionsPageReducer } from './reducer';
Expand All @@ -29,6 +35,7 @@ import { getListFetchError } from './selector';
jest.mock('../service');
const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock;
const deleteHostIsolationExceptionItemsMock = deleteHostIsolationExceptionItems as jest.Mock;
const createHostIsolationExceptionItemMock = createHostIsolationExceptionItem as jest.Mock;

const fakeCoreStart = coreMock.createStart({ basePath: '/mock' });

Expand Down Expand Up @@ -81,7 +88,7 @@ describe('Host isolation exceptions middleware', () => {
};

beforeEach(() => {
getHostIsolationExceptionItemsMock.mockClear();
getHostIsolationExceptionItemsMock.mockReset();
getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock);
});

Expand Down Expand Up @@ -145,11 +152,74 @@ describe('Host isolation exceptions middleware', () => {
});
});

describe('When adding an item to host isolation exceptions', () => {
let entry: CreateExceptionListItemSchema;
beforeEach(() => {
createHostIsolationExceptionItemMock.mockReset();
entry = {
...createEmptyHostIsolationException(),
name: 'test name',
description: 'description',
entries: [
{
field: 'destination.ip',
operator: 'included',
type: 'match',
value: '10.0.0.1',
},
],
};
});
it('should dispatch a form loading state when an entry is submited', async () => {
const waiter = spyMiddleware.waitForAction('hostIsolationExceptionsFormStateChanged', {
validate({ payload }) {
return isLoadingResourceState(payload);
},
});
store.dispatch({
type: 'hostIsolationExceptionsCreateEntry',
payload: entry,
});
await waiter;
});
it('should dispatch a form success state when an entry is confirmed by the API', async () => {
const waiter = spyMiddleware.waitForAction('hostIsolationExceptionsFormStateChanged', {
validate({ payload }) {
return isLoadedResourceState(payload);
},
});
store.dispatch({
type: 'hostIsolationExceptionsCreateEntry',
payload: entry,
});
await waiter;
expect(createHostIsolationExceptionItemMock).toHaveBeenCalledWith({
http: fakeCoreStart.http,
exception: entry,
});
});
it('should dispatch a form failure state when an entry is rejected by the API', async () => {
createHostIsolationExceptionItemMock.mockRejectedValue({
body: { message: 'error message', statusCode: 500, error: 'Not today' },
});
const waiter = spyMiddleware.waitForAction('hostIsolationExceptionsFormStateChanged', {
validate({ payload }) {
return isFailedResourceState(payload);
},
});
store.dispatch({
type: 'hostIsolationExceptionsCreateEntry',
payload: entry,
});
await waiter;
});
});

describe('When deleting an item from host isolation exceptions', () => {
beforeEach(() => {
deleteHostIsolationExceptionItemsMock.mockClear();
deleteHostIsolationExceptionItemsMock.mockReset();
deleteHostIsolationExceptionItemsMock.mockReturnValue(undefined);
getHostIsolationExceptionItemsMock.mockClear();
getHostIsolationExceptionItemsMock.mockReset();
getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock);
store.dispatch({
type: 'hostIsolationExceptionsMarkToDelete',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
*/

import {
CreateExceptionListItemSchema,
ExceptionListItemSchema,
FoundExceptionListItemSchema,
} from '@kbn/securitysolution-io-ts-list-types';
import { CoreStart, HttpSetup, HttpStart } from 'kibana/public';
import { matchPath } from 'react-router-dom';
import { transformNewItemOutput } from '@kbn/securitysolution-list-hooks';
import { AppLocation, Immutable } from '../../../../../common/endpoint/types';
import { ImmutableMiddleware, ImmutableMiddlewareAPI } from '../../../../common/store';
import { AppAction } from '../../../../common/store/actions';
Expand All @@ -20,7 +22,11 @@ import {
createFailedResourceState,
createLoadedResourceState,
} from '../../../state/async_resource_builders';
import { deleteHostIsolationExceptionItems, getHostIsolationExceptionItems } from '../service';
import {
deleteHostIsolationExceptionItems,
getHostIsolationExceptionItems,
createHostIsolationExceptionItem,
} from '../service';
import { HostIsolationExceptionsPageState } from '../types';
import { getCurrentListPageDataState, getCurrentLocation, getItemToDelete } from './selector';

Expand All @@ -39,12 +45,50 @@ export const createHostIsolationExceptionsPageMiddleware = (
if (action.type === 'userChangedUrl' && isHostIsolationExceptionsPage(action.payload)) {
loadHostIsolationExceptionsList(store, coreStart.http);
}

if (action.type === 'hostIsolationExceptionsCreateEntry') {
createHostIsolationException(store, coreStart.http);
}

if (action.type === 'hostIsolationExceptionsSubmitDelete') {
deleteHostIsolationExceptionsItem(store, coreStart.http);
}
};
};

async function createHostIsolationException(
store: ImmutableMiddlewareAPI<HostIsolationExceptionsPageState, AppAction>,
http: HttpStart
) {
const { dispatch } = store;
const entry = transformNewItemOutput(
store.getState().form.entry as CreateExceptionListItemSchema
);
dispatch({
type: 'hostIsolationExceptionsFormStateChanged',
payload: {
type: 'LoadingResourceState',
// @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830)
previousState: entry,
},
});
try {
const response = await createHostIsolationExceptionItem({
http,
exception: entry,
});
dispatch({
type: 'hostIsolationExceptionsFormStateChanged',
payload: createLoadedResourceState(response),
});
} catch (error) {
dispatch({
type: 'hostIsolationExceptionsFormStateChanged',
payload: createFailedResourceState<ExceptionListItemSchema>(error.body ?? error),
});
}
}

async function loadHostIsolationExceptionsList(
store: ImmutableMiddlewareAPI<HostIsolationExceptionsPageState, AppAction>,
http: HttpStart
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { initialHostIsolationExceptionsPageState } from './builders';
import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../common/constants';
import { hostIsolationExceptionsPageReducer } from './reducer';
import { getCurrentLocation } from './selector';
import { createEmptyHostIsolationException } from '../utils';

describe('Host Isolation Exceptions Reducer', () => {
let initialState: HostIsolationExceptionsPageState;
Expand Down Expand Up @@ -41,4 +42,13 @@ describe('Host Isolation Exceptions Reducer', () => {
});
});
});
it('should set an initial loading state when creating new entries', () => {
const entry = createEmptyHostIsolationException();
const result = hostIsolationExceptionsPageReducer(initialState, {
type: 'hostIsolationExceptionsCreateEntry',
payload: entry,
});
expect(result.form.status).toEqual({ type: 'UninitialisedResourceState' });
expect(result.form.entry).toBe(entry);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ export const hostIsolationExceptionsPageReducer: StateReducer = (
action
) => {
switch (action.type) {
case 'hostIsolationExceptionsCreateEntry': {
return {
...state,
form: {
entry: action.payload,
status: createUninitialisedResourceState(),
},
};
}
case 'hostIsolationExceptionsFormStateChanged': {
return {
...state,
form: {
...state.form,
status: action.payload,
},
};
}
case 'hostIsolationExceptionsPageDataChanged': {
return {
...state,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import type {
CreateExceptionListItemSchema,
ExceptionListItemSchema,
FoundExceptionListItemSchema,
} from '@kbn/securitysolution-io-ts-list-types';
Expand All @@ -27,4 +28,8 @@ export interface HostIsolationExceptionsPageState {
item?: ExceptionListItemSchema;
status: AsyncResourceState<ExceptionListItemSchema>;
};
form: {
entry?: CreateExceptionListItemSchema;
status: AsyncResourceState<ExceptionListItemSchema>;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolution-list-constants';
import ipaddr from 'ipaddr.js';

export function createEmptyHostIsolationException(): CreateExceptionListItemSchema {
return {
comments: [],
description: '',
entries: [
{
field: 'destination.ip',
operator: 'included',
type: 'match',
value: '',
},
],
item_id: undefined,
list_id: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID,
name: '',
namespace_type: 'agnostic',
os_types: ['windows', 'linux', 'macos'],
tags: ['policy:all'],
type: 'simple',
};
}

export function isValidIPv4OrCIDR(maybeIp: string): boolean {
try {
ipaddr.IPv4.parseCIDR(maybeIp);
return true;
} catch (e) {
return ipaddr.IPv4.isValid(maybeIp);
}
}
Loading

0 comments on commit 2ffbf6e

Please sign in to comment.