Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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