From b4ecec8c51e614618a371260b925a0739a9da086 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Fri, 1 Oct 2021 14:49:40 +0200 Subject: [PATCH 01/21] Initial empty form to add an exception --- .../host_isolation_exceptions/store/action.ts | 9 +- .../store/builders.ts | 6 + .../pages/host_isolation_exceptions/types.ts | 13 +- .../view/components/empty.tsx | 12 +- .../view/components/flyout.tsx | 123 ++++++++++++++++++ .../view/components/form.tsx | 11 ++ .../view/host_isolation_exceptions_list.tsx | 42 +++++- 7 files changed, 209 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/flyout.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts index 793c44ce79db2..86aaaa0f7e657 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts @@ -13,4 +13,11 @@ export type HostIsolationExceptionsPageDataChanged = payload: HostIsolationExceptionsPageState['entries']; }; -export type HostIsolationExceptionsPageAction = HostIsolationExceptionsPageDataChanged; +export type HostIsolationExceptionsFormStateChanged = + Action<'hostIsolationExceptionsFormStateChanged'> & { + payload: HostIsolationExceptionsPageState['form']['status']; + }; + +export type HostIsolationExceptionsPageAction = + | HostIsolationExceptionsPageDataChanged + | HostIsolationExceptionsFormStateChanged; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts index f5ea3c27bde7f..bde1b7fc89e07 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts @@ -16,4 +16,10 @@ export const initialHostIsolationExceptionsPageState = (): HostIsolationExceptio page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, filter: '', }, + form: { + entry: undefined, + status: createUninitialisedResourceState(), + hasNameError: false, + hasIpError: false, + }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts index 44f3d2a9df764..69257fbcac6e8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts @@ -5,7 +5,12 @@ * 2.0. */ -import type { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import type { + CreateExceptionListItemSchema, + ExceptionListItemSchema, + FoundExceptionListItemSchema, + UpdateExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; import { AsyncResourceState } from '../../state/async_resource_state'; export interface HostIsolationExceptionsPageLocation { @@ -20,4 +25,10 @@ export interface HostIsolationExceptionsPageLocation { export interface HostIsolationExceptionsPageState { entries: AsyncResourceState; location: HostIsolationExceptionsPageLocation; + form: { + entry: UpdateExceptionListItemSchema | CreateExceptionListItemSchema | undefined; + status: AsyncResourceState; + hasNameError: boolean; + hasIpError: boolean; + }; } diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/empty.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/empty.tsx index d7c512794173c..eb53268a9fbd8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/empty.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/empty.tsx @@ -7,7 +7,7 @@ import React, { memo } from 'react'; import styled, { css } from 'styled-components'; -import { EuiEmptyPrompt } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; const EmptyPrompt = styled(EuiEmptyPrompt)` @@ -16,7 +16,7 @@ const EmptyPrompt = styled(EuiEmptyPrompt)` `} `; -export const HostIsolationExceptionsEmptyState = memo<{}>(() => { +export const HostIsolationExceptionsEmptyState = memo<{ onAdd: () => void }>(({ onAdd }) => { return ( (() => { defaultMessage="There are currently no host isolation exceptions" /> } + actions={ + + + + } /> ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/flyout.tsx new file mode 100644 index 0000000000000..a44907beb1b93 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/flyout.tsx @@ -0,0 +1,123 @@ +/* + * 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 React, { memo, useMemo, useEffect, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { HostIsolationExceptionsPageAction } from '../../store/action'; +import { HostIsolationExceptionsForm } from './form'; +import { useHostIsolationExceptionsSelector } from '../hooks'; +import { + isLoadedResourceState, + isLoadingResourceState, +} from '../../../../state/async_resource_state'; + +export const HostIsolationExceptionsFlyout: React.FC<{ + type?: 'create' | 'edit'; + id?: string; + onCancel(): void; +}> = memo(({ onCancel }) => { + // useEventFiltersNotification(); + const dispatch = useDispatch>(); + + const formHasError = useHostIsolationExceptionsSelector( + (state) => state.form.hasNameError || state.form.hasIpError + ); + const creationInProgress = useHostIsolationExceptionsSelector((state) => + isLoadingResourceState(state.form.status) + ); + const creationSuccessful = useHostIsolationExceptionsSelector((state) => + isLoadedResourceState(state.form.status) + ); + + useEffect(() => { + if (creationSuccessful) { + onCancel(); + dispatch({ + type: 'hostIsolationExceptionsFormStateChanged', + payload: { + type: 'UninitialisedResourceState', + }, + }); + } + }, [creationSuccessful, onCancel, dispatch]); + + const handleOnCancel = useCallback(() => { + if (creationInProgress) return; + onCancel(); + }, [creationInProgress, onCancel]); + + const confirmButtonMemo = useMemo( + () => ( + {}} // TODO - actually create something + isLoading={creationInProgress} + > + + + ), + [formHasError, creationInProgress] + ); + + return ( + + + +

+ +

+
+
+ + + + + + + + + + + + + {confirmButtonMemo} + + +
+ ); +}); + +HostIsolationExceptionsFlyout.displayName = 'HostIsolationExceptionsFlyout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx new file mode 100644 index 0000000000000..09949bfa4dc25 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx @@ -0,0 +1,11 @@ +/* + * 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 React from 'react'; + +export const HostIsolationExceptionsForm = () => { + return

{'The form'}

; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index f6198e4e1aa54..f0b5ca533d7e8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -8,7 +8,7 @@ import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { i18n } from '@kbn/i18n'; import React, { useCallback } from 'react'; -import { EuiSpacer } from '@elastic/eui'; +import { EuiButton, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { ExceptionItem } from '../../../../common/components/exceptions/viewer/exception_item'; import { @@ -28,6 +28,7 @@ import { AdministrationListPage } from '../../../components/administration_list_ import { SearchExceptions } from '../../../components/search_exceptions'; import { ArtifactEntryCard, ArtifactEntryCardProps } from '../../../components/artifact_entry_card'; import { HostIsolationExceptionsEmptyState } from './components/empty'; +import { HostIsolationExceptionsFlyout } from './components/flyout'; type HostIsolationExceptionPaginatedContent = PaginatedContentProps< Immutable, @@ -41,6 +42,8 @@ export const HostIsolationExceptionsList = () => { const fetchError = useHostIsolationExceptionsSelector(getListFetchError); const location = useHostIsolationExceptionsSelector(getCurrentLocation); + const showFlyout = !!location.show; + const navigateCallback = useHostIsolationExceptionsNavigateCallback(); const handleOnSearch = useCallback( @@ -66,6 +69,24 @@ export const HostIsolationExceptionsList = () => { [navigateCallback] ); + const handleAddButtonClick = useCallback( + () => + navigateCallback({ + show: 'create', + id: undefined, + }), + [navigateCallback] + ); + + const handleCancelButtonClick = useCallback( + () => + navigateCallback({ + show: undefined, + id: undefined, + }), + [navigateCallback] + ); + return ( { defaultMessage="Host Isolation Exceptions" /> } - actions={[]} + actions={ + + + + } > + {showFlyout && } + { pagination={pagination} contentClassName="host-isolation-exceptions-container" data-test-subj="hostIsolationExceptionsContent" - noItemsMessage={} + noItemsMessage={} /> ); From 0fb4d41d93f074730284c587eac1749a43d7ed79 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Mon, 4 Oct 2021 11:26:27 +0200 Subject: [PATCH 02/21] WIP add form construction --- .../pages/host_isolation_exceptions/types.ts | 2 - .../view/components/form.tsx | 177 +++++++++++++++++- .../view/components/translations.ts | 64 +++++++ 3 files changed, 238 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/translations.ts diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts index 69257fbcac6e8..d9f0b7f81068d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts @@ -28,7 +28,5 @@ export interface HostIsolationExceptionsPageState { form: { entry: UpdateExceptionListItemSchema | CreateExceptionListItemSchema | undefined; status: AsyncResourceState; - hasNameError: boolean; - hasIpError: boolean; }; } diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx index 09949bfa4dc25..6dc46730d31dd 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx @@ -4,8 +4,179 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; -export const HostIsolationExceptionsForm = () => { - return

{'The form'}

; +import { + EuiFieldText, + EuiForm, + EuiFormRow, + EuiHorizontalRule, + EuiSpacer, + EuiText, + EuiTextArea, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback, useMemo, useState } from 'react'; +import { useHostIsolationExceptionsSelector } from '../hooks'; +import { + NAME_ERROR, + NAME_LABEL, + NAME_PLACEHOLDER, + DESCRIPTION_LABEL, + DESCRIPTION_PLACEHOLDER, + IP_ERROR, + IP_LABEL, + IP_PLACEHOLDER, +} from './translations'; + +export const HostIsolationExceptionsForm: React.FC<{}> = () => { + // const dispatch = useDispatch>(); + const exception = useHostIsolationExceptionsSelector((state) => state.form.entry); + const [hasBeenInputNameVisited, setHasBeenInputNameVisited] = useState(false); + const [hasBeenInputIpVisited, setHasBeenInputIpVisited] = useState(false); + const [hasNameError, setHasNameError] = useState(false); + const [hasIpError, setHasIpError] = useState(false); + const [exceptionName, setExeptionName] = useState(exception?.name); + const [exceptionDescription, setExeptionDescription] = useState(exception?.description); + // TODO fix this + const [exceptionIp, setExeptionIp] = useState(exception?.entries[0]?.value); + + const handleOnChangeName = useCallback( + (event: React.ChangeEvent) => { + const name = event.target.value; + if (!name.trim()) { + setHasNameError(true); + return; + } + setHasNameError(false); + setExeptionName(name); + }, + [setHasNameError, setExeptionName] + ); + + const handleOnIpChange = useCallback( + (event: React.ChangeEvent) => { + const ip = event.target.value; + // TODO validate IP somehow with CIDR + if (!ip.trim()) { + setHasIpError(true); + return; + } + setHasIpError(false); + setExeptionIp(ip); + }, + [setHasIpError, setExeptionIp] + ); + + const handleOnDescriptionChange = useCallback((event: React.ChangeEvent) => { + setExeptionDescription(event.target.value); + }, []); + + const nameInput = useMemo( + () => ( + + !hasBeenInputNameVisited && setHasBeenInputNameVisited(true)} + /> + + ), + [hasNameError, exceptionName, hasBeenInputNameVisited, handleOnChangeName] + ); + + const ipInput = useMemo( + () => ( + + !hasBeenInputIpVisited && setHasBeenInputIpVisited(true)} + /> + + ), + [hasIpError, hasBeenInputIpVisited, exceptionIp, handleOnIpChange] + ); + + // TOOD - this should be an area text + const descriptionInput = useMemo( + () => ( + + + + ), + [exceptionIp, handleOnDescriptionChange] + ); + + return ( + + +

+ +

+
+ + + + + {nameInput} + {descriptionInput} + + + +

+ +

+
+ + + + + {ipInput} +
+ ); }; + +HostIsolationExceptionsForm.displayName = 'HostIsolationExceptionsForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/translations.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/translations.ts new file mode 100644 index 0000000000000..df179c7a2221c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/translations.ts @@ -0,0 +1,64 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const NAME_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.name.placeholder', + { + defaultMessage: 'New IP', + } +); + +export const NAME_LABEL = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.name.label', + { + defaultMessage: 'Name your host isolation exceptions', + } +); + +export const NAME_ERROR = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.name.error', + { + defaultMessage: "The name can't be empty", + } +); + +export const DESCRIPTION_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.description.placeholder', + { + defaultMessage: 'Describe your Host Isolation Exception', + } +); + +export const DESCRIPTION_LABEL = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.description.label', + { + defaultMessage: 'Description', + } +); + +export const IP_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.ip.placeholder', + { + defaultMessage: 'Ex 0.0.0.0/24', + } +); + +export const IP_LABEL = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.ip.label', + { + defaultMessage: 'Enter IP Address', + } +); + +export const IP_ERROR = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.ip.error', + { + defaultMessage: 'The ip is invalid. Only IPv4 with optional CIDR is supported', + } +); From c73b47fdea25a165bf3ca1751eff8be563cbe945 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Mon, 4 Oct 2021 16:14:45 +0200 Subject: [PATCH 03/21] Add code to handle the add form --- .../host_isolation_exceptions/store/action.ts | 5 ++ .../store/builders.ts | 2 - .../pages/host_isolation_exceptions/utils.ts | 33 +++++++++ .../view/components/form.tsx | 71 ++++++++++++------- .../{flyout.tsx => form_flyout.tsx} | 30 +++++--- .../view/host_isolation_exceptions_list.tsx | 4 +- 6 files changed, 108 insertions(+), 37 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts rename x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/{flyout.tsx => form_flyout.tsx} (78%) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts index 86aaaa0f7e657..fcc177479be2b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts @@ -18,6 +18,11 @@ export type HostIsolationExceptionsFormStateChanged = payload: HostIsolationExceptionsPageState['form']['status']; }; +export type HostIsolationExceptionsFormEntryChanged = + Action<'hostIslationExceptionsFormEntryChanged'> & { + payload: HostIsolationExceptionsPageState['form']['entry']; + }; + export type HostIsolationExceptionsPageAction = | HostIsolationExceptionsPageDataChanged | HostIsolationExceptionsFormStateChanged; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts index bde1b7fc89e07..4ac9e99740e13 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts @@ -19,7 +19,5 @@ export const initialHostIsolationExceptionsPageState = (): HostIsolationExceptio form: { entry: undefined, status: createUninitialisedResourceState(), - hasNameError: false, - hasIpError: false, }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts new file mode 100644 index 0000000000000..4df3d12d51ee3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts @@ -0,0 +1,33 @@ +/* + * 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 uuid from 'uuid'; +import { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolution-list-constants'; + +export const createEmptyHostIsolationException = (): CreateExceptionListItemSchema => ({ + comments: [], + description: '', + entries: [ + { + field: 'destination.ip', + operator: 'included', + type: 'match', + value: '', + }, + ], + item_id: undefined, + list_id: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, + meta: { + temporaryUuid: uuid.v4(), + }, + name: '', + namespace_type: 'agnostic', + tags: ['policy:all'], + type: 'simple', + os_types: ['windows', 'linux', 'macos'], +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx index 6dc46730d31dd..57a2746bd1415 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx @@ -16,8 +16,8 @@ import { EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useCallback, useMemo, useState } from 'react'; -import { useHostIsolationExceptionsSelector } from '../hooks'; +import { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { NAME_ERROR, NAME_LABEL, @@ -29,17 +29,26 @@ import { IP_PLACEHOLDER, } from './translations'; -export const HostIsolationExceptionsForm: React.FC<{}> = () => { - // const dispatch = useDispatch>(); - const exception = useHostIsolationExceptionsSelector((state) => state.form.entry); +interface ExceptionIpEntry { + field: 'destination.ip'; + operator: 'included'; + type: 'match'; + value: ''; +} + +export const HostIsolationExceptionsForm: React.FC<{ + exception: CreateExceptionListItemSchema; + onError: (error: boolean) => void; + onChange: (exception: CreateExceptionListItemSchema) => void; +}> = memo(({ exception, onError, onChange }) => { const [hasBeenInputNameVisited, setHasBeenInputNameVisited] = useState(false); const [hasBeenInputIpVisited, setHasBeenInputIpVisited] = useState(false); const [hasNameError, setHasNameError] = useState(false); const [hasIpError, setHasIpError] = useState(false); - const [exceptionName, setExeptionName] = useState(exception?.name); - const [exceptionDescription, setExeptionDescription] = useState(exception?.description); - // TODO fix this - const [exceptionIp, setExeptionIp] = useState(exception?.entries[0]?.value); + + useEffect(() => { + onError(hasNameError || hasIpError); + }, [hasNameError, hasIpError, onError]); const handleOnChangeName = useCallback( (event: React.ChangeEvent) => { @@ -49,9 +58,9 @@ export const HostIsolationExceptionsForm: React.FC<{}> = () => { return; } setHasNameError(false); - setExeptionName(name); + onChange({ ...exception, name }); }, - [setHasNameError, setExeptionName] + [exception, onChange] ); const handleOnIpChange = useCallback( @@ -63,14 +72,27 @@ export const HostIsolationExceptionsForm: React.FC<{}> = () => { return; } setHasIpError(false); - setExeptionIp(ip); + onChange({ + ...exception, + entries: [ + { + field: 'destination.ip', + operator: 'included', + type: 'match', + value: ip, + }, + ], + }); }, - [setHasIpError, setExeptionIp] + [exception, onChange] ); - const handleOnDescriptionChange = useCallback((event: React.ChangeEvent) => { - setExeptionDescription(event.target.value); - }, []); + const handleOnDescriptionChange = useCallback( + (event: React.ChangeEvent) => { + onChange({ ...exception, description: event.target.value }); + }, + [exception, onChange] + ); const nameInput = useMemo( () => ( @@ -83,7 +105,7 @@ export const HostIsolationExceptionsForm: React.FC<{}> = () => { = () => { /> ), - [hasNameError, exceptionName, hasBeenInputNameVisited, handleOnChangeName] + [hasNameError, hasBeenInputNameVisited, exception.name, handleOnChangeName] ); const ipInput = useMemo( @@ -107,7 +129,7 @@ export const HostIsolationExceptionsForm: React.FC<{}> = () => { = () => { /> ), - [hasIpError, hasBeenInputIpVisited, exceptionIp, handleOnIpChange] + [hasIpError, hasBeenInputIpVisited, exception.entries, handleOnIpChange] ); - // TOOD - this should be an area text const descriptionInput = useMemo( () => ( = () => { /> ), - [exceptionIp, handleOnDescriptionChange] + [exception.description, handleOnDescriptionChange] ); return ( @@ -151,7 +172,7 @@ export const HostIsolationExceptionsForm: React.FC<{}> = () => { @@ -177,6 +198,6 @@ export const HostIsolationExceptionsForm: React.FC<{}> = () => { {ipInput} ); -}; +}); HostIsolationExceptionsForm.displayName = 'HostIsolationExceptionsForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx similarity index 78% rename from x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/flyout.tsx rename to x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx index a44907beb1b93..435bc0aa70bde 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useMemo, useEffect, useCallback } from 'react'; +import React, { memo, useMemo, useEffect, useCallback, useState } from 'react'; import { useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; @@ -21,6 +21,8 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; +import { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { Loader } from '../../../../../common/components/loader'; import { HostIsolationExceptionsPageAction } from '../../store/action'; import { HostIsolationExceptionsForm } from './form'; import { useHostIsolationExceptionsSelector } from '../hooks'; @@ -28,8 +30,9 @@ import { isLoadedResourceState, isLoadingResourceState, } from '../../../../state/async_resource_state'; +import { createEmptyHostIsolationException } from '../../utils'; -export const HostIsolationExceptionsFlyout: React.FC<{ +export const HostIsolationExceptionsFormFlyout: React.FC<{ type?: 'create' | 'edit'; id?: string; onCancel(): void; @@ -37,9 +40,14 @@ export const HostIsolationExceptionsFlyout: React.FC<{ // useEventFiltersNotification(); const dispatch = useDispatch>(); - const formHasError = useHostIsolationExceptionsSelector( - (state) => state.form.hasNameError || state.form.hasIpError - ); + const [formHasError, setFormHasError] = useState(true); + + const [exception, setException] = useState(undefined); + + useEffect(() => { + setException(createEmptyHostIsolationException()); + }, []); + const creationInProgress = useHostIsolationExceptionsSelector((state) => isLoadingResourceState(state.form.status) ); @@ -82,7 +90,7 @@ export const HostIsolationExceptionsFlyout: React.FC<{ [formHasError, creationInProgress] ); - return ( + return exception ? ( - + @@ -117,7 +129,9 @@ export const HostIsolationExceptionsFlyout: React.FC<{ + ) : ( + ); }); -HostIsolationExceptionsFlyout.displayName = 'HostIsolationExceptionsFlyout'; +HostIsolationExceptionsFormFlyout.displayName = 'HostIsolationExceptionsFormFlyout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index f0b5ca533d7e8..baeebd461e779 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -28,7 +28,7 @@ import { AdministrationListPage } from '../../../components/administration_list_ import { SearchExceptions } from '../../../components/search_exceptions'; import { ArtifactEntryCard, ArtifactEntryCardProps } from '../../../components/artifact_entry_card'; import { HostIsolationExceptionsEmptyState } from './components/empty'; -import { HostIsolationExceptionsFlyout } from './components/flyout'; +import { HostIsolationExceptionsFormFlyout } from './components/form_flyout'; type HostIsolationExceptionPaginatedContent = PaginatedContentProps< Immutable, @@ -110,7 +110,7 @@ export const HostIsolationExceptionsList = () => { } > - {showFlyout && } + {showFlyout && } Date: Tue, 5 Oct 2021 09:45:20 +0200 Subject: [PATCH 04/21] WIP create entry --- .../pages/host_isolation_exceptions/store/action.ts | 5 +++++ .../view/components/form.tsx | 4 ++-- .../view/components/form_flyout.tsx | 11 +++++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts index fcc177479be2b..d341c30325294 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts @@ -23,6 +23,11 @@ export type HostIsolationExceptionsFormEntryChanged = payload: HostIsolationExceptionsPageState['form']['entry']; }; +export type HostIsolationExceptionsCreateEntry = Action<'hostIsolationExceptionsCreateEntry'> & { + payload: HostIsolationExceptionsPageState['form']['entry']; +}; + export type HostIsolationExceptionsPageAction = | HostIsolationExceptionsPageDataChanged + | HostIsolationExceptionsCreateEntry | HostIsolationExceptionsFormStateChanged; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx index 57a2746bd1415..cea74dc69145d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx @@ -43,8 +43,8 @@ export const HostIsolationExceptionsForm: React.FC<{ }> = memo(({ exception, onError, onChange }) => { const [hasBeenInputNameVisited, setHasBeenInputNameVisited] = useState(false); const [hasBeenInputIpVisited, setHasBeenInputIpVisited] = useState(false); - const [hasNameError, setHasNameError] = useState(false); - const [hasIpError, setHasIpError] = useState(false); + const [hasNameError, setHasNameError] = useState(true); + const [hasIpError, setHasIpError] = useState(true); useEffect(() => { onError(hasNameError || hasIpError); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx index 435bc0aa70bde..39a8b2fef355e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx @@ -72,13 +72,20 @@ export const HostIsolationExceptionsFormFlyout: React.FC<{ onCancel(); }, [creationInProgress, onCancel]); + const handleOnSubmit = useCallback(() => { + dispatch({ + type: 'hostIsolationExceptionsCreateEntry', + payload: exception, + }); + }, [dispatch, exception]); + const confirmButtonMemo = useMemo( () => ( {}} // TODO - actually create something + onClick={handleOnSubmit} isLoading={creationInProgress} > ), - [formHasError, creationInProgress] + [formHasError, creationInProgress, handleOnSubmit] ); return exception ? ( From 1754d41874be53e310e4edf7ce142f0a52ef32bd Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 5 Oct 2021 11:26:41 +0200 Subject: [PATCH 05/21] Working add --- .../host_isolation_exceptions/service.ts | 14 ++++++ .../store/middleware.ts | 46 ++++++++++++++++++- .../store/reducer.ts | 19 ++++++++ .../pages/host_isolation_exceptions/types.ts | 2 +- .../pages/host_isolation_exceptions/utils.ts | 4 -- 5 files changed, 78 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts index 85545303c7df0..c757fdc3eec58 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts @@ -6,6 +6,7 @@ */ import { + CreateExceptionListItemSchema, ExceptionListItemSchema, FoundExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; @@ -64,3 +65,16 @@ export async function getHostIsolationExceptionItems({ }); return entries; } + +export async function crateHostIsolationExceptionItem({ + http, + exception, +}: { + http: HttpStart; + exception: CreateExceptionListItemSchema; +}): Promise { + await ensureHostIsolationExceptionsListExists(http); + return http.post(EXCEPTION_LIST_ITEM_URL, { + body: JSON.stringify(exception), + }); +} diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts index 1df0ef229d2ef..3598ba75e2bbb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts @@ -5,9 +5,14 @@ * 2.0. */ -import { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { + CreateExceptionListItemSchema, + ExceptionListItemSchema, + FoundExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; import { CoreStart, HttpStart } from 'kibana/public'; import { matchPath } from 'react-router-dom'; +import { transformNewItemOutput, transformOutput } from '@kbn/securitysolution-list-hooks'; import { AppLocation, Immutable } from '../../../../../common/endpoint/types'; import { ImmutableMiddleware, ImmutableMiddlewareAPI } from '../../../../common/store'; import { AppAction } from '../../../../common/store/actions'; @@ -17,7 +22,7 @@ import { createFailedResourceState, createLoadedResourceState, } from '../../../state/async_resource_builders'; -import { getHostIsolationExceptionItems } from '../service'; +import { crateHostIsolationExceptionItem, getHostIsolationExceptionItems } from '../service'; import { HostIsolationExceptionsPageState } from '../types'; import { getCurrentListPageDataState, getCurrentLocation } from './selector'; @@ -36,9 +41,46 @@ export const createHostIsolationExceptionsPageMiddleware = ( if (action.type === 'userChangedUrl' && isHostIsolationExceptionsPage(action.payload)) { loadHostIsolationExceptionsList(store, coreStart.http); } + + if (action.type === 'hostIsolationExceptionsCreateEntry') { + createHostIsolationException(store, coreStart.http); + } }; }; +async function createHostIsolationException( + store: ImmutableMiddlewareAPI, + 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 crateHostIsolationExceptionItem({ + http, + exception: entry, + }); + dispatch({ + type: 'hostIsolationExceptionsFormStateChanged', + payload: createLoadedResourceState(response), + }); + } catch (error) { + dispatch({ + type: 'hostIsolationExceptionsFormStateChanged', + payload: createFailedResourceState(error.body ?? error), + }); + } +} + async function loadHostIsolationExceptionsList( store: ImmutableMiddlewareAPI, http: HttpStart diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.ts index 1bce76c1bfd06..2ae7dfd3569a2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.ts @@ -16,6 +16,7 @@ import { HostIsolationExceptionsPageState } from '../types'; import { initialHostIsolationExceptionsPageState } from './builders'; import { MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../common/constants'; import { UserChangedUrl } from '../../../../common/store/routing/action'; +import { createUninitialisedResourceState } from '../../../state'; type StateReducer = ImmutableReducer; type CaseReducer = ( @@ -37,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, diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts index d9f0b7f81068d..a79d31873a522 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts @@ -26,7 +26,7 @@ export interface HostIsolationExceptionsPageState { entries: AsyncResourceState; location: HostIsolationExceptionsPageLocation; form: { - entry: UpdateExceptionListItemSchema | CreateExceptionListItemSchema | undefined; + entry: CreateExceptionListItemSchema | undefined; status: AsyncResourceState; }; } diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts index 4df3d12d51ee3..7f5166dfc4bb7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts @@ -5,7 +5,6 @@ * 2.0. */ -import uuid from 'uuid'; import { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolution-list-constants'; @@ -22,9 +21,6 @@ export const createEmptyHostIsolationException = (): CreateExceptionListItemSche ], item_id: undefined, list_id: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, - meta: { - temporaryUuid: uuid.v4(), - }, name: '', namespace_type: 'agnostic', tags: ['policy:all'], From 6fc871ff03b4444f066b20491aeb30dcad0cf10d Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 5 Oct 2021 12:09:24 +0200 Subject: [PATCH 06/21] Add toast to create and failure --- .../view/components/form_flyout.tsx | 78 +++++++++++++------ 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx index 39a8b2fef355e..e8337f8793e3b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx @@ -5,48 +5,42 @@ * 2.0. */ -import React, { memo, useMemo, useEffect, useCallback, useState } from 'react'; -import { useDispatch } from 'react-redux'; -import { Dispatch } from 'redux'; - -import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiFlyout, - EuiFlyoutHeader, - EuiTitle, - EuiFlyoutBody, - EuiFlyoutFooter, EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiTitle, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; import { Loader } from '../../../../../common/components/loader'; -import { HostIsolationExceptionsPageAction } from '../../store/action'; -import { HostIsolationExceptionsForm } from './form'; -import { useHostIsolationExceptionsSelector } from '../hooks'; +import { useToasts } from '../../../../../common/lib/kibana'; import { + isFailedResourceState, isLoadedResourceState, isLoadingResourceState, } from '../../../../state/async_resource_state'; +import { HostIsolationExceptionsPageAction } from '../../store/action'; import { createEmptyHostIsolationException } from '../../utils'; +import { useHostIsolationExceptionsSelector } from '../hooks'; +import { HostIsolationExceptionsForm } from './form'; export const HostIsolationExceptionsFormFlyout: React.FC<{ type?: 'create' | 'edit'; id?: string; onCancel(): void; }> = memo(({ onCancel }) => { - // useEventFiltersNotification(); const dispatch = useDispatch>(); - - const [formHasError, setFormHasError] = useState(true); - - const [exception, setException] = useState(undefined); - - useEffect(() => { - setException(createEmptyHostIsolationException()); - }, []); + const toasts = useToasts(); const creationInProgress = useHostIsolationExceptionsSelector((state) => isLoadingResourceState(state.form.status) @@ -54,6 +48,16 @@ export const HostIsolationExceptionsFormFlyout: React.FC<{ const creationSuccessful = useHostIsolationExceptionsSelector((state) => isLoadedResourceState(state.form.status) ); + const creationFailure = useHostIsolationExceptionsSelector((state) => + isFailedResourceState(state.form.status) + ); + + const [formHasError, setFormHasError] = useState(true); + const [exception, setException] = useState(undefined); + + useEffect(() => { + setException(createEmptyHostIsolationException()); + }, []); useEffect(() => { if (creationSuccessful) { @@ -64,8 +68,36 @@ export const HostIsolationExceptionsFormFlyout: React.FC<{ type: 'UninitialisedResourceState', }, }); + toasts.addSuccess( + i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.creationSuccessToastTitle', + { + defaultMessage: '"{name}" has been added to the host isolation exceptions list.', + values: { name: exception?.name }, + } + ) + ); + } + }, [creationSuccessful, onCancel, dispatch, toasts, exception?.name]); + + useEffect(() => { + if (creationFailure) { + dispatch({ + type: 'hostIsolationExceptionsFormStateChanged', + payload: { + type: 'UninitialisedResourceState', + }, + }); + toasts.addDanger( + i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.creationFailureToastTitle', + { + defaultMessage: 'There was an error creating the exception', + } + ) + ); } - }, [creationSuccessful, onCancel, dispatch]); + }, [dispatch, toasts, creationFailure]); const handleOnCancel = useCallback(() => { if (creationInProgress) return; From 0378a5739118388b466e6073d72b19b08431214a Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 5 Oct 2021 14:37:03 +0200 Subject: [PATCH 07/21] Add validation for ipv4 and CIDR format --- .../pages/host_isolation_exceptions/utils.ts | 50 ++++++++++++------- .../view/components/form.tsx | 10 ++-- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts index 7f5166dfc4bb7..729ac5afcb0fd 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts @@ -7,23 +7,35 @@ 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 const createEmptyHostIsolationException = (): CreateExceptionListItemSchema => ({ - 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', - tags: ['policy:all'], - type: 'simple', - os_types: ['windows', 'linux', 'macos'], -}); +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', + tags: ['policy:all'], + type: 'simple', + os_types: ['windows', 'linux', 'macos'], + }; +} + +export function isValidIPv4OrCIDR(maybeIp: string): boolean { + try { + ipaddr.IPv4.parseCIDR(maybeIp); + return true; + } catch (e) { + return ipaddr.IPv4.isValid(maybeIp); + } +} diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx index cea74dc69145d..85ddbc958c1d8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx @@ -18,15 +18,16 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { isValidIPv4OrCIDR } from '../../utils'; import { - NAME_ERROR, - NAME_LABEL, - NAME_PLACEHOLDER, DESCRIPTION_LABEL, DESCRIPTION_PLACEHOLDER, IP_ERROR, IP_LABEL, IP_PLACEHOLDER, + NAME_ERROR, + NAME_LABEL, + NAME_PLACEHOLDER, } from './translations'; interface ExceptionIpEntry { @@ -66,8 +67,7 @@ export const HostIsolationExceptionsForm: React.FC<{ const handleOnIpChange = useCallback( (event: React.ChangeEvent) => { const ip = event.target.value; - // TODO validate IP somehow with CIDR - if (!ip.trim()) { + if (!isValidIPv4OrCIDR(ip)) { setHasIpError(true); return; } From 64669d8bc1ab664fa9f3fe60868368a1ab5e1a08 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 5 Oct 2021 15:59:58 +0200 Subject: [PATCH 08/21] Reload the list of exceptions after adding --- .../pages/host_isolation_exceptions/store/middleware.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts index 3598ba75e2bbb..33c5365d35398 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts @@ -12,7 +12,7 @@ import { } from '@kbn/securitysolution-io-ts-list-types'; import { CoreStart, HttpStart } from 'kibana/public'; import { matchPath } from 'react-router-dom'; -import { transformNewItemOutput, transformOutput } from '@kbn/securitysolution-list-hooks'; +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'; @@ -73,6 +73,7 @@ async function createHostIsolationException( type: 'hostIsolationExceptionsFormStateChanged', payload: createLoadedResourceState(response), }); + loadHostIsolationExceptionsList(store, http); } catch (error) { dispatch({ type: 'hostIsolationExceptionsFormStateChanged', From dcbb5df4ac7dc9f07a8acc228503095a76c7dc13 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 5 Oct 2021 16:02:10 +0200 Subject: [PATCH 09/21] Remove unused import --- .../public/management/pages/host_isolation_exceptions/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts index a79d31873a522..d9679b37d8c2d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts @@ -9,7 +9,6 @@ import type { CreateExceptionListItemSchema, ExceptionListItemSchema, FoundExceptionListItemSchema, - UpdateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; import { AsyncResourceState } from '../../state/async_resource_state'; From 02cbe0bd17180f2a6131ac0c8a1369f93e1b11c3 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 6 Oct 2021 10:43:21 +0200 Subject: [PATCH 10/21] Replace mockclear with mockreset --- .../host_isolation_exceptions/service.ts | 2 +- .../store/middleware.test.ts | 80 +++++++++++++++++-- .../store/middleware.ts | 4 +- .../view/components/form.test.tsx | 6 ++ 4 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts index af0d791d0f03a..8af353a3c9531 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts @@ -66,7 +66,7 @@ export async function getHostIsolationExceptionItems({ return entries; } -export async function crateHostIsolationExceptionItem({ +export async function createHostIsolationExceptionItem({ http, exception, }: { diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.test.ts index 984794e074ebb..266853fdab5e2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.test.ts @@ -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, @@ -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'; @@ -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' }); @@ -81,7 +88,7 @@ describe('Host isolation exceptions middleware', () => { }; beforeEach(() => { - getHostIsolationExceptionItemsMock.mockClear(); + getHostIsolationExceptionItemsMock.mockReset(); getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock); }); @@ -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', diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts index 9a307cc92d8fc..5b8bd3632922f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts @@ -25,7 +25,7 @@ import { import { deleteHostIsolationExceptionItems, getHostIsolationExceptionItems, - crateHostIsolationExceptionItem, + createHostIsolationExceptionItem, } from '../service'; import { HostIsolationExceptionsPageState } from '../types'; import { getCurrentListPageDataState, getCurrentLocation, getItemToDelete } from './selector'; @@ -73,7 +73,7 @@ async function createHostIsolationException( }, }); try { - const response = await crateHostIsolationExceptionItem({ + const response = await createHostIsolationExceptionItem({ http, exception: entry, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx new file mode 100644 index 0000000000000..1fec1c76430eb --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx @@ -0,0 +1,6 @@ +/* + * 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. + */ From becaa1a2ec5251801e6a1084bb1b0a362563c2ea Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 6 Oct 2021 10:55:39 +0200 Subject: [PATCH 11/21] Add tests for reducer and add host Isolation exception --- .../host_isolation_exceptions/store/reducer.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.test.ts index 211b03f36d965..72a786afd6af6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.test.ts @@ -11,6 +11,8 @@ import { initialHostIsolationExceptionsPageState } from './builders'; import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../common/constants'; import { hostIsolationExceptionsPageReducer } from './reducer'; import { getCurrentLocation } from './selector'; +import { HostIsolationExceptionsCreateEntry } from './action'; +import { createEmptyHostIsolationException } from '../utils'; describe('Host Isolation Exceptions Reducer', () => { let initialState: HostIsolationExceptionsPageState; @@ -41,4 +43,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); + }); }); From 82a47aac21dff2a76432121d1832cf6bbff4ca88 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 6 Oct 2021 11:21:10 +0200 Subject: [PATCH 12/21] Fix TS error --- .../pages/host_isolation_exceptions/store/reducer.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.test.ts index 72a786afd6af6..98b459fac41d3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.test.ts @@ -11,7 +11,6 @@ import { initialHostIsolationExceptionsPageState } from './builders'; import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../common/constants'; import { hostIsolationExceptionsPageReducer } from './reducer'; import { getCurrentLocation } from './selector'; -import { HostIsolationExceptionsCreateEntry } from './action'; import { createEmptyHostIsolationException } from '../utils'; describe('Host Isolation Exceptions Reducer', () => { From d18264e592e862d93d5cbaeee0f7f82cf2be2f39 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 6 Oct 2021 12:08:37 +0200 Subject: [PATCH 13/21] Firsts test for form --- .../view/components/form.test.tsx | 77 +++++++++++++++++++ .../view/components/form.tsx | 3 + 2 files changed, 80 insertions(+) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx index 1fec1c76430eb..dd8278c6d1f52 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx @@ -4,3 +4,80 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +import { createEmptyHostIsolationException } from '../../utils'; +import { HostIsolationExceptionsForm } from './form'; +import React from 'react'; +import { + AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../../common/mock/endpoint'; +import { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import userEvent from '@testing-library/user-event'; +import { act } from 'react-dom/test-utils'; + +describe('When on the host isolation exceptions add entry form', () => { + let render: ( + exception: CreateExceptionListItemSchema + ) => ReturnType; + let renderResult: ReturnType; + const onChange = jest.fn(); + const onError = jest.fn(); + + beforeEach(() => { + onChange.mockReset(); + onError.mockReset(); + const mockedContext = createAppRootMockRenderer(); + render = (exception: CreateExceptionListItemSchema) => { + return mockedContext.render( + + ); + }; + }); + + describe('When creating a new exception', () => { + beforeEach(() => { + const newException = createEmptyHostIsolationException(); + renderResult = render(newException); + }); + it('should render the form with empty inputs', () => { + expect(renderResult.getByTestId('hostIsolationExceptions-form-name-input')).toHaveValue(''); + expect(renderResult.getByTestId('hostIsolationExceptions-form-ip-input')).toHaveValue(''); + expect( + renderResult.getByTestId('hostIsolationExceptions-form-description-input') + ).toHaveValue(''); + }); + it('should call onError with true when a wrong value is introduced', () => { + const ipInput = renderResult.getByTestId('hostIsolationExceptions-form-ip-input'); + userEvent.type(ipInput, 'not an ip'); + expect(onError).toHaveBeenCalledWith(true); + }); + it('should call onError with false when a correct values are introduced', () => { + const ipInput = renderResult.getByTestId('hostIsolationExceptions-form-ip-input'); + const nameInput = renderResult.getByTestId('hostIsolationExceptions-form-name-input'); + + userEvent.type(nameInput, 'test name'); + userEvent.type(ipInput, '10.0.0.1'); + + expect(onError).toHaveBeenLastCalledWith(false); + }); + it('should call onChange when a value is introduced in a field', () => { + const ipInput = renderResult.getByTestId('hostIsolationExceptions-form-ip-input'); + userEvent.type(ipInput, '10.0.0.1'); + expect(onChange).toHaveBeenCalledWith({ + comments: [], + description: '', + entries: [ + { field: 'destination.ip', operator: 'included', type: 'match', value: '10.0.0.1' }, + ], + item_id: undefined, + list_id: 'endpoint_host_isolation_exceptions', + name: '', + namespace_type: 'agnostic', + os_types: ['windows', 'linux', 'macos'], + tags: ['policy:all'], + type: 'simple', + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx index 85ddbc958c1d8..84263f9d07c81 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx @@ -111,6 +111,7 @@ export const HostIsolationExceptionsForm: React.FC<{ aria-label={NAME_PLACEHOLDER} required={hasBeenInputNameVisited} maxLength={256} + data-test-subj="hostIsolationExceptions-form-name-input" onBlur={() => !hasBeenInputNameVisited && setHasBeenInputNameVisited(true)} /> @@ -135,6 +136,7 @@ export const HostIsolationExceptionsForm: React.FC<{ aria-label={IP_PLACEHOLDER} required={hasBeenInputIpVisited} maxLength={256} + data-test-subj="hostIsolationExceptions-form-ip-input" onBlur={() => !hasBeenInputIpVisited && setHasBeenInputIpVisited(true)} /> @@ -151,6 +153,7 @@ export const HostIsolationExceptionsForm: React.FC<{ defaultValue={exception.description ?? ''} onChange={handleOnDescriptionChange} fullWidth + data-test-subj="hostIsolationExceptions-form-description-input" aria-label={DESCRIPTION_PLACEHOLDER} maxLength={256} /> From 3ccdf9e17dde78789a18b9dfcf6a15ce31383ee3 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 6 Oct 2021 13:32:40 +0200 Subject: [PATCH 14/21] Tests for form --- .../view/components/form.test.tsx | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx index dd8278c6d1f52..3b796f9f48554 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx @@ -15,6 +15,7 @@ import { import { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import userEvent from '@testing-library/user-event'; import { act } from 'react-dom/test-utils'; +import { fireEvent } from '@testing-library/dom'; describe('When on the host isolation exceptions add entry form', () => { let render: ( @@ -36,8 +37,9 @@ describe('When on the host isolation exceptions add entry form', () => { }); describe('When creating a new exception', () => { + let newException: CreateExceptionListItemSchema; beforeEach(() => { - const newException = createEmptyHostIsolationException(); + newException = createEmptyHostIsolationException(); renderResult = render(newException); }); it('should render the form with empty inputs', () => { @@ -47,7 +49,7 @@ describe('When on the host isolation exceptions add entry form', () => { renderResult.getByTestId('hostIsolationExceptions-form-description-input') ).toHaveValue(''); }); - it('should call onError with true when a wrong value is introduced', () => { + it('should call onError with true when a wrong ip value is introduced', () => { const ipInput = renderResult.getByTestId('hostIsolationExceptions-form-ip-input'); userEvent.type(ipInput, 'not an ip'); expect(onError).toHaveBeenCalledWith(true); @@ -64,19 +66,11 @@ describe('When on the host isolation exceptions add entry form', () => { it('should call onChange when a value is introduced in a field', () => { const ipInput = renderResult.getByTestId('hostIsolationExceptions-form-ip-input'); userEvent.type(ipInput, '10.0.0.1'); - expect(onChange).toHaveBeenCalledWith({ - comments: [], - description: '', + expect(onChange).toHaveBeenLastCalledWith({ + ...newException, entries: [ { field: 'destination.ip', operator: 'included', type: 'match', value: '10.0.0.1' }, ], - item_id: undefined, - list_id: 'endpoint_host_isolation_exceptions', - name: '', - namespace_type: 'agnostic', - os_types: ['windows', 'linux', 'macos'], - tags: ['policy:all'], - type: 'simple', }); }); }); From 138fca70bf106d2c52599b248308b556cdea105c Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 6 Oct 2021 14:54:24 +0200 Subject: [PATCH 15/21] Remove unused dependencies --- .../host_isolation_exceptions/view/components/form.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx index 3b796f9f48554..b06449de69d8c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx @@ -14,8 +14,6 @@ import { } from '../../../../../common/mock/endpoint'; import { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import userEvent from '@testing-library/user-event'; -import { act } from 'react-dom/test-utils'; -import { fireEvent } from '@testing-library/dom'; describe('When on the host isolation exceptions add entry form', () => { let render: ( From 346dbf26ec89188de0047f17365236cd8d87dc2d Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 6 Oct 2021 15:46:47 +0200 Subject: [PATCH 16/21] Add tests for for the form flyout --- .../view/components/form_flyout.test.tsx | 114 ++++++++++++++++++ .../view/components/form_flyout.tsx | 2 +- 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx new file mode 100644 index 0000000000000..9c0f925be68e8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx @@ -0,0 +1,114 @@ +/* + * 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 React from 'react'; +import { + AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../../common/mock/endpoint'; +import userEvent from '@testing-library/user-event'; +import { HostIsolationExceptionsFormFlyout } from './form_flyout'; +import { act } from 'react-dom/test-utils'; + +jest.mock('../../service.ts'); + +describe('When on the host isolation exceptions flyout form', () => { + let mockedContext: AppContextTestRender; + let render: () => ReturnType; + let renderResult: ReturnType; + let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction']; + const onCancel = jest.fn(); + + // const createHostIsolationExceptionItemMock = createHostIsolationExceptionItem as jest.mock; + + beforeEach(() => { + onCancel.mockReset(); + mockedContext = createAppRootMockRenderer(); + render = () => { + return mockedContext.render(); + }; + waitForAction = mockedContext.middlewareSpy.waitForAction; + }); + + describe('When creating a new exception', () => { + describe('with invalida data', () => { + it('should show disabled buttons when the form first load', () => { + renderResult = render(); + expect(renderResult.getByTestId('add-exception-cancel-button')).not.toHaveAttribute( + 'disabled' + ); + expect(renderResult.getByTestId('add-exception-confirm-button')).toHaveAttribute( + 'disabled' + ); + }); + }); + describe('with valid data', () => { + beforeEach(() => { + renderResult = render(); + const ipInput = renderResult.getByTestId('hostIsolationExceptions-form-ip-input'); + const nameInput = renderResult.getByTestId('hostIsolationExceptions-form-name-input'); + userEvent.type(nameInput, 'test name'); + userEvent.type(ipInput, '10.0.0.1'); + }); + it('should show enable buttons when the form is valid', () => { + expect(renderResult.getByTestId('add-exception-cancel-button')).not.toHaveAttribute( + 'disabled' + ); + expect(renderResult.getByTestId('add-exception-confirm-button')).not.toHaveAttribute( + 'disabled' + ); + }); + it('should submit the entry data when submit is pressed with valid data', async () => { + const confirmButton = renderResult.getByTestId('add-exception-confirm-button'); + expect(confirmButton).not.toHaveAttribute('disabled'); + const waiter = waitForAction('hostIsolationExceptionsCreateEntry'); + userEvent.click(confirmButton); + await waiter; + }); + it('should disable the submit button when an operation is in progress', () => { + act(() => { + mockedContext.store.dispatch({ + type: 'hostIsolationExceptionsFormStateChanged', + payload: { + type: 'LoadingResourceState', + previousState: { type: 'UninitialisedResourceState' }, + }, + }); + }); + const confirmButton = renderResult.getByTestId('add-exception-confirm-button'); + expect(confirmButton).toHaveAttribute('disabled'); + }); + it('should show a toast and call onCancel when the operation is finished', () => { + act(() => { + mockedContext.store.dispatch({ + type: 'hostIsolationExceptionsFormStateChanged', + payload: { + type: 'LoadedResourceState', + previousState: { type: 'UninitialisedResourceState' }, + }, + }); + }); + expect(onCancel).toHaveBeenCalled(); + expect(mockedContext.coreStart.notifications.toasts.addSuccess).toHaveBeenCalled(); + }); + it('should show an error toast operation fails and enable the submit button', () => { + act(() => { + mockedContext.store.dispatch({ + type: 'hostIsolationExceptionsFormStateChanged', + payload: { + type: 'FailedResourceState', + previousState: { type: 'UninitialisedResourceState' }, + }, + }); + }); + expect(mockedContext.coreStart.notifications.toasts.addDanger).toHaveBeenCalled(); + const confirmButton = renderResult.getByTestId('add-exception-confirm-button'); + expect(confirmButton).not.toHaveAttribute('disabled'); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx index e8337f8793e3b..bb957d47b9897 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx @@ -157,7 +157,7 @@ export const HostIsolationExceptionsFormFlyout: React.FC<{ - + Date: Wed, 6 Oct 2021 16:10:04 +0200 Subject: [PATCH 17/21] Add tests for the add button --- .../view/components/form_flyout.tsx | 2 -- .../host_isolation_exceptions_list.test.tsx | 22 +++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx index bb957d47b9897..f18b3bbf9981e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx @@ -35,8 +35,6 @@ import { useHostIsolationExceptionsSelector } from '../hooks'; import { HostIsolationExceptionsForm } from './form'; export const HostIsolationExceptionsFormFlyout: React.FC<{ - type?: 'create' | 'edit'; - id?: string; onCancel(): void; }> = memo(({ onCancel }) => { const dispatch = useDispatch>(); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx index 53b8bc33c252f..ac472fdae4d7b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx @@ -5,16 +5,18 @@ * 2.0. */ -import React from 'react'; import { act } from '@testing-library/react'; -import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../common/constants'; -import { HostIsolationExceptionsList } from './host_isolation_exceptions_list'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { isFailedResourceState, isLoadedResourceState } from '../../../state'; import { getHostIsolationExceptionItems } from '../service'; -import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; +import { HostIsolationExceptionsList } from './host_isolation_exceptions_list'; jest.mock('../service'); + const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock; describe('When on the host isolation exceptions page', () => { @@ -103,5 +105,17 @@ describe('When on the host isolation exceptions page', () => { ).toEqual(' Server is too far away'); }); }); + it('should show the create flyout when the add button is pressed', () => { + render(); + act(() => { + userEvent.click(renderResult.getByTestId('hostIsolationExceptionsListAddButton')); + }); + expect(renderResult.getByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeTruthy(); + }); + it('should show the create flyout when the show location is create', () => { + history.push(`${HOST_ISOLATION_EXCEPTIONS_PATH}?show=create`); + render(); + expect(renderResult.getByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeTruthy(); + }); }); }); From e5e8b125db140c802a023e782e2b2f682bd3f291 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Thu, 7 Oct 2021 10:49:18 +0200 Subject: [PATCH 18/21] Fix PR comments --- .../pages/host_isolation_exceptions/store/middleware.ts | 1 - .../view/components/form_flyout.tsx | 8 +------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts index 5b8bd3632922f..bbc754e8155b0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts @@ -81,7 +81,6 @@ async function createHostIsolationException( type: 'hostIsolationExceptionsFormStateChanged', payload: createLoadedResourceState(response), }); - loadHostIsolationExceptionsList(store, http); } catch (error) { dispatch({ type: 'hostIsolationExceptionsFormStateChanged', diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx index f18b3bbf9981e..74a21145c5238 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx @@ -80,12 +80,6 @@ export const HostIsolationExceptionsFormFlyout: React.FC<{ useEffect(() => { if (creationFailure) { - dispatch({ - type: 'hostIsolationExceptionsFormStateChanged', - payload: { - type: 'UninitialisedResourceState', - }, - }); toasts.addDanger( i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.form.creationFailureToastTitle', @@ -129,7 +123,7 @@ export const HostIsolationExceptionsFormFlyout: React.FC<{ return exception ? ( From 21dd6c7b2651388c98091497254f5f0ac7f02892 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Thu, 7 Oct 2021 11:05:21 +0200 Subject: [PATCH 19/21] Handle onCancel directly on the form flyout --- .../view/components/form_flyout.test.tsx | 10 ++++----- .../view/components/form_flyout.tsx | 22 ++++++++++++++----- .../view/host_isolation_exceptions_list.tsx | 11 +--------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx index 9c0f925be68e8..8a199b5c6361f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx @@ -13,6 +13,7 @@ import { import userEvent from '@testing-library/user-event'; import { HostIsolationExceptionsFormFlyout } from './form_flyout'; import { act } from 'react-dom/test-utils'; +import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../../common/constants'; jest.mock('../../service.ts'); @@ -21,15 +22,13 @@ describe('When on the host isolation exceptions flyout form', () => { let render: () => ReturnType; let renderResult: ReturnType; let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction']; - const onCancel = jest.fn(); // const createHostIsolationExceptionItemMock = createHostIsolationExceptionItem as jest.mock; beforeEach(() => { - onCancel.mockReset(); mockedContext = createAppRootMockRenderer(); render = () => { - return mockedContext.render(); + return mockedContext.render(); }; waitForAction = mockedContext.middlewareSpy.waitForAction; }); @@ -82,7 +81,8 @@ describe('When on the host isolation exceptions flyout form', () => { const confirmButton = renderResult.getByTestId('add-exception-confirm-button'); expect(confirmButton).toHaveAttribute('disabled'); }); - it('should show a toast and call onCancel when the operation is finished', () => { + it('should show a toast and close the flyout when the operation is finished', () => { + mockedContext.history.push(`${HOST_ISOLATION_EXCEPTIONS_PATH}?show=create`); act(() => { mockedContext.store.dispatch({ type: 'hostIsolationExceptionsFormStateChanged', @@ -92,8 +92,8 @@ describe('When on the host isolation exceptions flyout form', () => { }, }); }); - expect(onCancel).toHaveBeenCalled(); expect(mockedContext.coreStart.notifications.toasts.addSuccess).toHaveBeenCalled(); + expect(mockedContext.history.location.search).toBe(''); }); it('should show an error toast operation fails and enable the submit button', () => { act(() => { diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx index 74a21145c5238..c51c169b791c2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx @@ -31,12 +31,13 @@ import { } from '../../../../state/async_resource_state'; import { HostIsolationExceptionsPageAction } from '../../store/action'; import { createEmptyHostIsolationException } from '../../utils'; -import { useHostIsolationExceptionsSelector } from '../hooks'; +import { + useHostIsolationExceptionsNavigateCallback, + useHostIsolationExceptionsSelector, +} from '../hooks'; import { HostIsolationExceptionsForm } from './form'; -export const HostIsolationExceptionsFormFlyout: React.FC<{ - onCancel(): void; -}> = memo(({ onCancel }) => { +export const HostIsolationExceptionsFormFlyout: React.FC<{}> = memo(() => { const dispatch = useDispatch>(); const toasts = useToasts(); @@ -50,9 +51,20 @@ export const HostIsolationExceptionsFormFlyout: React.FC<{ isFailedResourceState(state.form.status) ); + const navigateCallback = useHostIsolationExceptionsNavigateCallback(); + const [formHasError, setFormHasError] = useState(true); const [exception, setException] = useState(undefined); + const onCancel = useCallback( + () => + navigateCallback({ + show: undefined, + id: undefined, + }), + [navigateCallback] + ); + useEffect(() => { setException(createEmptyHostIsolationException()); }, []); @@ -94,7 +106,7 @@ export const HostIsolationExceptionsFormFlyout: React.FC<{ const handleOnCancel = useCallback(() => { if (creationInProgress) return; onCancel(); - }, [creationInProgress, onCancel]); + }, [creationInProgress]); const handleOnSubmit = useCallback(() => { dispatch({ diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index fea0e71e50649..cfb0121396e24 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -104,15 +104,6 @@ export const HostIsolationExceptionsList = () => { [navigateCallback] ); - const handleCancelButtonClick = useCallback( - () => - navigateCallback({ - show: undefined, - id: undefined, - }), - [navigateCallback] - ); - return ( { } > - {showFlyout && } + {showFlyout && } Date: Thu, 7 Oct 2021 11:15:58 +0200 Subject: [PATCH 20/21] Fix dependency issue for hook --- .../host_isolation_exceptions/view/components/form_flyout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx index c51c169b791c2..5502a1b8ea2b1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx @@ -106,7 +106,7 @@ export const HostIsolationExceptionsFormFlyout: React.FC<{}> = memo(() => { const handleOnCancel = useCallback(() => { if (creationInProgress) return; onCancel(); - }, [creationInProgress]); + }, [creationInProgress, onCancel]); const handleOnSubmit = useCallback(() => { dispatch({ From 6a697eaaa98cf1574f8c50bd6165876fd0ca59c3 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Mon, 11 Oct 2021 13:17:26 +0200 Subject: [PATCH 21/21] Fix PR comments --- .../management/pages/host_isolation_exceptions/store/action.ts | 2 +- .../public/management/pages/host_isolation_exceptions/types.ts | 2 +- .../public/management/pages/host_isolation_exceptions/utils.ts | 2 +- .../view/components/form_flyout.test.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts index 8c3ab2efb54de..a5fae36486f98 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts @@ -20,7 +20,7 @@ export type HostIsolationExceptionsFormStateChanged = }; export type HostIsolationExceptionsFormEntryChanged = - Action<'hostIslationExceptionsFormEntryChanged'> & { + Action<'hostIsolationExceptionsFormEntryChanged'> & { payload: HostIsolationExceptionsPageState['form']['entry']; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts index d9c0552418cb7..1a74042fb652e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts @@ -29,7 +29,7 @@ export interface HostIsolationExceptionsPageState { status: AsyncResourceState; }; form: { - entry: CreateExceptionListItemSchema | undefined; + entry?: CreateExceptionListItemSchema; status: AsyncResourceState; }; } diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts index 729ac5afcb0fd..bfb1ac048e286 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts @@ -25,9 +25,9 @@ export function createEmptyHostIsolationException(): CreateExceptionListItemSche list_id: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, name: '', namespace_type: 'agnostic', + os_types: ['windows', 'linux', 'macos'], tags: ['policy:all'], type: 'simple', - os_types: ['windows', 'linux', 'macos'], }; } diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx index 8a199b5c6361f..6cfc9f56beadf 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx @@ -34,7 +34,7 @@ describe('When on the host isolation exceptions flyout form', () => { }); describe('When creating a new exception', () => { - describe('with invalida data', () => { + describe('with invalid data', () => { it('should show disabled buttons when the form first load', () => { renderResult = render(); expect(renderResult.getByTestId('add-exception-cancel-button')).not.toHaveAttribute(