From 363e011898eb308d6918fdab4eff52342b958c25 Mon Sep 17 00:00:00 2001 From: yctercero Date: Fri, 12 Aug 2022 13:29:49 -0700 Subject: [PATCH 01/30] initial changes done, need to add tests, fix types, cleanup --- .../find_exception_references_schema.ts | 21 ++ .../components/exceptions/translations.ts | 6 +- .../exceptions/use_find_references.tsx | 73 +++++ .../exception_item_card_conditions.tsx | 25 +- .../exception_item_card_meta.tsx | 134 +++++++--- .../viewer/exception_item_card/index.tsx | 18 +- .../exception_item_card/translations.ts | 24 +- .../exceptions/viewer/exceptions_utility.tsx | 107 +++----- .../viewer/exceptions_viewer_header.tsx | 154 ++--------- .../viewer/exceptions_viewer_items.tsx | 119 ++++---- .../components/exceptions/viewer/index.tsx | 253 ++++++++++-------- .../exceptions/viewer/no_exception_items.tsx | 62 +++++ .../exceptions/viewer/no_search_results.tsx | 33 +++ .../components/exceptions/viewer/reducer.ts | 7 +- .../exceptions/viewer/translations.ts | 85 ++++++ .../containers/detection_engine/rules/api.ts | 26 ++ .../detection_engine/rules/details/index.tsx | 144 ++++++---- .../rules/details/translations.ts | 11 +- .../rules/find_rule_exceptions_route.ts | 95 +++++++ .../security_solution/server/routes/index.ts | 2 + 20 files changed, 912 insertions(+), 487 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_references_schema.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/use_find_references.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/no_exception_items.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/no_search_results.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/translations.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_references_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_references_schema.ts new file mode 100644 index 0000000000000..e4e23c86344ac --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_references_schema.ts @@ -0,0 +1,21 @@ +/* + * 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 * as t from 'io-ts'; +import { NonEmptyStringArray } from '@kbn/securitysolution-io-ts-types'; + +export const findExceptionReferencesOnRuleSchema = t.exact( + t.partial({ + list_ids: NonEmptyStringArray, + list_list_ids: NonEmptyStringArray, + namespace_types: NonEmptyStringArray, + }) +); + +export type FindExceptionReferencesOnRuleSchema = t.OutputOf< + typeof findExceptionReferencesOnRuleSchema +>; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts index 2372e063b48cf..d9b6fc8739b90 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts @@ -74,7 +74,7 @@ export const ADD_EXCEPTION_LABEL = i18n.translate( export const ADD_TO_ENDPOINT_LIST = i18n.translate( 'xpack.securitySolution.exceptions.viewer.addToEndpointListLabel', { - defaultMessage: 'Add Endpoint exception', + defaultMessage: 'Add endpoint exception', } ); @@ -122,9 +122,9 @@ export const DELETE_EXCEPTION_ERROR = i18n.translate( ); export const ITEMS_PER_PAGE = (items: number) => - i18n.translate('xpack.securitySolution.exceptions.exceptionsPaginationLabel', { + i18n.translate('xpack.securitySolution.exceptions.exceptionItemsPaginationLabel', { values: { items }, - defaultMessage: 'Items per page: {items}', + defaultMessage: 'Rows per page: {items}', }); export const NUMBER_OF_ITEMS = (items: number) => diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_find_references.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_find_references.tsx new file mode 100644 index 0000000000000..444d3d0ea64e5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_find_references.tsx @@ -0,0 +1,73 @@ +/* + * 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 { useEffect, useState, useCallback } from 'react'; +import type { ListArray, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; + +import { findRuleExceptionReferences } from '../../../detections/containers/detection_engine/rules/api'; +import { useKibana } from '../../lib/kibana'; + +export type ReturnUseFindExceptionListReferences = [boolean, unknown[]]; + +/** + * Hook for finding what rules reference a set of exception lists + * @param listReferences static id of + */ +export const useFindExceptionListReferences = ( + ruleExceptionLists: ListArray +): ReturnUseFindExceptionListReferences => { + const { http } = useKibana().services; + const [isLoading, setIsLoading] = useState(true); + const [references, setReferences] = useState(null); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + const refs = ruleExceptionLists.map((list) => { + return { + id: list.id, + listId: list.list_id, + type: list.namespace_type, + }; + }); + + const findReferences = async () => { + try { + setIsLoading(true); + + const addOrUpdateItems = await findRuleExceptionReferences({ + lists: refs, + http, + signal: abortCtrl.signal, + }); + + if (isSubscribed) { + setIsLoading(false); + setReferences(addOrUpdateItems); + } + } catch (error) { + if (isSubscribed) { + setIsLoading(false); + } + } + }; + + if (refs.length === 0 && isSubscribed) { + setIsLoading(false); + setReferences(null); + } else { + findReferences(); + } + + return (): void => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [http, ruleExceptionLists]); + + return [isLoading, references]; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx index 62c1ba013e7ae..b84c3aef017ed 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx @@ -6,7 +6,14 @@ */ import React, { memo, useMemo, useCallback } from 'react'; -import { EuiExpression, EuiToken, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import { + EuiExpression, + EuiToken, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiPanel, +} from '@elastic/eui'; import styled from 'styled-components'; import type { EntryExists, @@ -59,6 +66,12 @@ const StyledCondition = styled('span')` margin-right: 6px; `; +const StyledConditionContent = styled(EuiPanel)` + border: 1px; + border-color: #d3dae6; + border-style: solid; +`; + export interface CriteriaConditionsProps { entries: ExceptionListItemSchema['entries']; dataTestSubj: string; @@ -163,7 +176,13 @@ export const ExceptionItemCardConditions = memo( const operator = 'operator' in entry ? entry.operator : ''; return ( -
+
( />
{nestedEntries != null && getNestedEntriesContent(type, nestedEntries)} -
+ ); })} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx index 3e9cf5e68d95e..b47a27a20665e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx @@ -5,24 +5,82 @@ * 2.0. */ -import React, { memo } from 'react'; -import { EuiAvatar, EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import React, { memo, useMemo, useState } from 'react'; +import type { EuiContextMenuPanelProps } from '@elastic/eui'; +import { + EuiBadge, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, + EuiText, + EuiButtonEmpty, + EuiPopover, +} from '@elastic/eui'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import styled from 'styled-components'; import * as i18n from './translations'; -import { FormattedDate, FormattedRelativePreferenceDate } from '../../../formatted_date'; +import { FormattedDate } from '../../../formatted_date'; +import { LinkAnchor } from '../../../links'; +import { APP_UI_ID, SecurityPageName } from '../../../../../../common/constants'; +import { useKibana } from '../../../../lib/kibana'; +import { getRuleDetailsUrl } from '../../../link_to/redirect_to_detection_engine'; +import { useGetSecuritySolutionUrl } from '../../../link_to'; -const StyledCondition = styled('div')` - padding-top: 4px !important; +const StyledFlexItem = styled(EuiFlexItem)` + border-right: 1px solid #d3dae6; + padding: 4px 12px 4px 0; `; + export interface ExceptionItemCardMetaInfoProps { item: ExceptionListItemSchema; + references: unknown; dataTestSubj: string; } export const ExceptionItemCardMetaInfo = memo( - ({ item, dataTestSubj }) => { + ({ item, references, dataTestSubj }) => { + const { navigateToApp } = useKibana().services.application; + const getSecuritySolutionUrl = useGetSecuritySolutionUrl(); + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onAffectedRulesClick = () => setIsPopoverOpen((isOpen) => !isOpen); + const onClosePopover = () => setIsPopoverOpen(false); + + const itemActions = useMemo((): EuiContextMenuPanelProps['items'] => { + if (references == null) { + return []; + } + return references.map((reference) => ( + + + void }) => { + ev.preventDefault(); + navigateToApp(APP_UI_ID, { + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsUrl(reference.id), + }); + }} + href={getSecuritySolutionUrl({ + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsUrl(reference.id), + })} + > + {reference.name} + + + + )); + }, [references, dataTestSubj, getSecuritySolutionUrl, navigateToApp]); + return ( ( gutterSize="s" data-test-subj={dataTestSubj} > - + ( value2={item.created_by} dataTestSubj={`${dataTestSubj}-createdBy`} /> - - + + - - - } + value1={} value2={item.updated_by} dataTestSubj={`${dataTestSubj}-updatedBy`} /> - + + {references != null && ( + + + {i18n.AFFECTED_RULES(references.length)} + + } + panelPaddingSize="none" + isOpen={isPopoverOpen} + closePopover={onClosePopover} + data-test-subj={`${dataTestSubj}-items`} + > + + + + )} ); } @@ -73,34 +144,27 @@ interface MetaInfoDetailsProps { const MetaInfoDetails = memo(({ label, value1, value2, dataTestSubj }) => { return ( - - - {label} - - - {value1} + {label} - + + {value1} + + + + {i18n.EXCEPTION_ITEM_META_BY} - - - - + {value2} - + diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx index 322050e27e4b8..a00ae6d4fd5df 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import React, { useMemo, useCallback } from 'react'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { getFormattedComments } from '../../helpers'; import type { ExceptionListItemIdentifiers } from '../../types'; @@ -28,16 +29,20 @@ import { ExceptionItemCardMetaInfo } from './exception_item_card_meta'; export interface ExceptionItemProps { loadingItemIds: ExceptionListItemIdentifiers[]; exceptionItem: ExceptionListItemSchema; + listType: ExceptionListTypeEnum; onDeleteException: (arg: ExceptionListItemIdentifiers) => void; onEditException: (item: ExceptionListItemSchema) => void; disableActions: boolean; dataTestSubj: string; + ruleReferences: unknown; } const ExceptionItemCardComponent = ({ disableActions, loadingItemIds, exceptionItem, + listType, + ruleReferences, onDeleteException, onEditException, dataTestSubj, @@ -73,14 +78,20 @@ const ExceptionItemCardComponent = ({ actions={[ { key: 'edit', - icon: 'pencil', - label: i18n.EXCEPTION_ITEM_EDIT_BUTTON, + icon: 'controlsHorizontal', + label: + listType === ExceptionListTypeEnum.ENDPOINT + ? i18n.ENDPOINT_EXCEPTION_ITEM_EDIT_BUTTON + : i18n.EXCEPTION_ITEM_EDIT_BUTTON, onClick: handleEdit, }, { key: 'delete', icon: 'trash', - label: i18n.EXCEPTION_ITEM_DELETE_BUTTON, + label: + listType === ExceptionListTypeEnum.ENDPOINT + ? i18n.ENDPOINT_EXCEPTION_ITEM_DELETE_BUTTON + : i18n.EXCEPTION_ITEM_DELETE_BUTTON, onClick: handleDelete, }, ]} @@ -91,6 +102,7 @@ const ExceptionItemCardComponent = ({ diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/translations.ts index 5d46243697355..ccdd5eebf3b8c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/translations.ts @@ -10,14 +10,28 @@ import { i18n } from '@kbn/i18n'; export const EXCEPTION_ITEM_EDIT_BUTTON = i18n.translate( 'xpack.securitySolution.exceptions.exceptionItem.editItemButton', { - defaultMessage: 'Edit item', + defaultMessage: 'Edit rule exception', } ); export const EXCEPTION_ITEM_DELETE_BUTTON = i18n.translate( 'xpack.securitySolution.exceptions.exceptionItem.deleteItemButton', { - defaultMessage: 'Delete item', + defaultMessage: 'Delete rule exception', + } +); + +export const ENDPOINT_EXCEPTION_ITEM_EDIT_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.endpoint.editItemButton', + { + defaultMessage: 'Edit endpoint exception', + } +); + +export const ENDPOINT_EXCEPTION_ITEM_DELETE_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.endpoint.deleteItemButton', + { + defaultMessage: 'Delete endpoint exception', } ); @@ -159,3 +173,9 @@ export const OS_MAC = i18n.translate( defaultMessage: 'Mac', } ); + +export const AFFECTED_RULES = (numRules: number) => + i18n.translate('xpack.securitySolution.exceptions.exceptionItem.affectedRules', { + values: { numRules }, + defaultMessage: 'Affects {numRules} {numRules, plural, =1 {rule} other {rules}}', + }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx index fd720377a1b1e..b025e54e97a51 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx @@ -6,42 +6,35 @@ */ import React from 'react'; -import { EuiText, EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; -import * as i18n from '../translations'; +import { FormattedMessage } from '@kbn/i18n-react'; import type { ExceptionsPagination } from '../types'; -import { - UtilityBar, - UtilityBarSection, - UtilityBarGroup, - UtilityBarText, - UtilityBarAction, -} from '../../utility_bar'; +import { UtilityBar, UtilityBarSection, UtilityBarGroup, UtilityBarText } from '../../utility_bar'; +import { FormattedRelativePreferenceDate } from '../../formatted_date'; -const StyledText = styled(EuiText)` - font-style: italic; +const StyledText = styled.span` + font-weight: bold; `; const MyUtilities = styled(EuiFlexGroup)` height: 50px; `; +const StyledCondition = styled.span` + display: inline-block !important; + vertical-align: middle !important; +`; + interface ExceptionsViewerUtilityProps { pagination: ExceptionsPagination; - showEndpointListsOnly: boolean; - showDetectionsListsOnly: boolean; - ruleSettingsUrl: string; - onRefreshClick: () => void; + lastUpdated: string | number | null; } const ExceptionsViewerUtilityComponent: React.FC = ({ pagination, - showEndpointListsOnly, - showDetectionsListsOnly, - ruleSettingsUrl, - onRefreshClick, + lastUpdated, }): JSX.Element => ( @@ -49,60 +42,38 @@ const ExceptionsViewerUtilityComponent: React.FC = - {i18n.SHOWING_EXCEPTIONS(pagination.totalItemCount ?? 0)} + {`1-${pagination.totalItemCount}`}, + partTwo: {`${pagination.totalItemCount}`}, + }} + /> - - - - {i18n.REFRESH} - - - - {showEndpointListsOnly && ( - - - - ), - }} - /> - )} - {showDetectionsListsOnly && ( - - - - ), - }} - /> - )} - + + + + + ), + }} + /> + ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx index dc234dc0a7ef4..8a350af5e9f1a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx @@ -5,18 +5,8 @@ * 2.0. */ -import type { EuiContextMenuPanelDescriptor } from '@elastic/eui'; -import { - EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiContextMenu, - EuiButton, - EuiFilterGroup, - EuiFilterButton, -} from '@elastic/eui'; import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import * as i18n from '../translations'; @@ -24,9 +14,7 @@ import type { Filter } from '../types'; interface ExceptionsViewerHeaderProps { isInitLoading: boolean; - supportedListTypes: ExceptionListTypeEnum[]; - detectionsListItems: number; - endpointListItems: number; + listType: ExceptionListTypeEnum; onFilterChange: (arg: Partial) => void; onAddExceptionClick: (type: ExceptionListTypeEnum) => void; } @@ -36,17 +24,12 @@ interface ExceptionsViewerHeaderProps { */ const ExceptionsViewerHeaderComponent = ({ isInitLoading, - supportedListTypes, - detectionsListItems, - endpointListItems, + listType, onFilterChange, onAddExceptionClick, }: ExceptionsViewerHeaderProps): JSX.Element => { const [filter, setFilter] = useState(''); const [tags, setTags] = useState([]); - const [showDetectionsListsOnly, setShowDetectionsList] = useState(false); - const [showEndpointListsOnly, setShowEndpointList] = useState(false); - const [isAddExceptionMenuOpen, setAddExceptionMenuOpen] = useState(false); useEffect((): void => { onFilterChange({ @@ -54,25 +37,10 @@ const ExceptionsViewerHeaderComponent = ({ pagination: { pageIndex: 0, }, - showDetectionsListsOnly, - showEndpointListsOnly, + showDetectionsListsOnly: listType !== ExceptionListTypeEnum.ENDPOINT, + showEndpointListsOnly: listType === ExceptionListTypeEnum.ENDPOINT, }); - }, [filter, tags, showDetectionsListsOnly, showEndpointListsOnly, onFilterChange]); - - const onAddExceptionDropdownClick = useCallback( - (): void => setAddExceptionMenuOpen(!isAddExceptionMenuOpen), - [setAddExceptionMenuOpen, isAddExceptionMenuOpen] - ); - - const handleDetectionsListClick = useCallback((): void => { - setShowDetectionsList(!showDetectionsListsOnly); - setShowEndpointList(false); - }, [showDetectionsListsOnly, setShowDetectionsList, setShowEndpointList]); - - const handleEndpointListClick = useCallback((): void => { - setShowEndpointList(!showEndpointListsOnly); - setShowDetectionsList(false); - }, [showEndpointListsOnly, setShowEndpointList, setShowDetectionsList]); + }, [filter, tags, onFilterChange, listType]); const handleOnSearch = useCallback( (searchValue: string): void => { @@ -90,34 +58,15 @@ const ExceptionsViewerHeaderComponent = ({ [setTags, setFilter] ); - const onAddException = useCallback( - (type: ExceptionListTypeEnum): void => { - onAddExceptionClick(type); - setAddExceptionMenuOpen(false); - }, - [onAddExceptionClick, setAddExceptionMenuOpen] - ); + const handleAddException = useCallback(() => { + onAddExceptionClick(listType); + }, [onAddExceptionClick, listType]); - const addExceptionButtonOptions = useMemo( - (): EuiContextMenuPanelDescriptor[] => [ - { - id: 0, - items: [ - { - name: i18n.ADD_TO_ENDPOINT_LIST, - onClick: () => onAddException(ExceptionListTypeEnum.ENDPOINT), - 'data-test-subj': 'addEndpointExceptionBtn', - }, - { - name: i18n.ADD_TO_DETECTIONS_LIST, - onClick: () => onAddException(ExceptionListTypeEnum.DETECTION), - 'data-test-subj': 'addDetectionsExceptionBtn', - }, - ], - }, - ], - [onAddException] - ); + const addExceptionButtonText = useMemo(() => { + return listType === ExceptionListTypeEnum.ENDPOINT + ? i18n.ADD_TO_ENDPOINT_LIST + : i18n.ADD_TO_DETECTIONS_LIST; + }, [listType]); return ( @@ -133,71 +82,16 @@ const ExceptionsViewerHeaderComponent = ({ /> - {supportedListTypes.length === 1 && ( - - onAddException(supportedListTypes[0])} - isDisabled={isInitLoading} - fill - > - {i18n.ADD_EXCEPTION_LABEL} - - - )} - - {supportedListTypes.length > 1 && ( - - - - - - {i18n.DETECTION_LIST} - {detectionsListItems != null ? ` (${detectionsListItems})` : ''} - - - {i18n.ENDPOINT_LIST} - {endpointListItems != null ? ` (${endpointListItems})` : ''} - - - - - - - {i18n.ADD_EXCEPTION_LABEL} - - } - isOpen={isAddExceptionMenuOpen} - closePopover={onAddExceptionDropdownClick} - anchorPosition="downCenter" - panelPaddingSize="none" - repositionOnScroll - > - - - - - - )} + + + {addExceptionButtonText} + + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx index e1d91ed0a0580..d6b9b743017af 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx @@ -6,96 +6,99 @@ */ import React from 'react'; -import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiLoadingLogo } from '@elastic/eui'; import styled from 'styled-components'; -import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import * as i18n from '../translations'; +import type { + ExceptionListItemSchema, + ExceptionListTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; +import * as i18n from './translations'; import { ExceptionItemCard } from './exception_item_card'; import type { ExceptionListItemIdentifiers } from '../types'; +import { ExeptionItemsViewerEmptySearchResults } from './no_search_results'; +import { ExeptionItemsViewerNoItems } from './no_exception_items'; const MyFlexItem = styled(EuiFlexItem)` margin: ${({ theme }) => `${theme.eui.euiSize} 0`}; - &:first-child { margin: ${({ theme }) => `${theme.eui.euiSizeXS} 0 ${theme.eui.euiSize}`}; } `; -const MyExceptionItemContainer = styled(EuiFlexGroup)` - margin: ${({ theme }) => `0 ${theme.eui.euiSize} ${theme.eui.euiSize} 0`}; -`; - -interface ExceptionsViewerItemsProps { +interface ExceptionItemsViewerProps { showEmpty: boolean; showNoResults: boolean; isInitLoading: boolean; disableActions: boolean; exceptions: ExceptionListItemSchema[]; loadingItemIds: ExceptionListItemIdentifiers[]; + listType: ExceptionListTypeEnum; + ruleReferences: unknown; + onCreateExceptionListItem: () => void; onDeleteException: (arg: ExceptionListItemIdentifiers) => void; onEditExceptionItem: (item: ExceptionListItemSchema) => void; } -const ExceptionsViewerItemsComponent: React.FC = ({ +const ExceptionItemsViewerComponent: React.FC = ({ showEmpty, showNoResults, isInitLoading, exceptions, loadingItemIds, + listType, + disableActions, + ruleReferences, onDeleteException, onEditExceptionItem, - disableActions, -}): JSX.Element => ( - - {showEmpty || showNoResults || isInitLoading ? ( - + onCreateExceptionListItem, +}): JSX.Element => { + return ( + + {isInitLoading && ( - {showNoResults ? '' : i18n.EXCEPTION_EMPTY_PROMPT_TITLE} - - } - body={ -

- {showNoResults - ? i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY - : i18n.EXCEPTION_EMPTY_PROMPT_BODY} -

- } - data-test-subj="exceptionsEmptyPrompt" + icon={} + title={

{i18n.EXCEPTION_LOADING_TITLE}

} + data-test-subj="exceptionsLoadingPrompt" + /> + )} + {showNoResults && } + {showEmpty && ( + -
- ) : ( - - - {!isInitLoading && - exceptions.length > 0 && - exceptions.map((exception) => ( - - - - ))} - - - )} -
-); + )} + {!showEmpty && !showNoResults && !isInitLoading && ( + + + {!isInitLoading && + exceptions.length > 0 && + exceptions.map((exception) => ( + + + + ))} + + + )} +
+ ); +}; -ExceptionsViewerItemsComponent.displayName = 'ExceptionsViewerItemsComponent'; +ExceptionItemsViewerComponent.displayName = 'ExceptionItemsViewerComponent'; -export const ExceptionsViewerItems = React.memo(ExceptionsViewerItemsComponent); +export const ExceptionsViewerItems = React.memo(ExceptionItemsViewerComponent); ExceptionsViewerItems.displayName = 'ExceptionsViewerItems'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index e724a546f8054..b47e19bd3953d 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -6,21 +6,21 @@ */ import React, { useCallback, useEffect, useReducer, useState } from 'react'; -import { EuiSpacer } from '@elastic/eui'; +import { EuiPanel, EuiSpacer } from '@elastic/eui'; import uuid from 'uuid'; import type { - ExceptionListTypeEnum, ExceptionListItemSchema, ExceptionListIdentifiers, UseExceptionListItemsSuccess, } from '@kbn/securitysolution-io-ts-list-types'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { useApi, useExceptionListItems } from '@kbn/securitysolution-list-hooks'; -import * as i18n from '../translations'; + +import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; import { useStateToaster } from '../../toasters'; import { useUserData } from '../../../../detections/components/user_info'; import { useKibana } from '../../../lib/kibana'; -import { Panel } from '../../panel'; import { Loader } from '../../loader'; import { ExceptionsViewerHeader } from './exceptions_viewer_header'; import type { ExceptionListItemIdentifiers, Filter } from '../types'; @@ -32,6 +32,8 @@ import { ExceptionsViewerUtility } from './exceptions_utility'; import { ExceptionsViewerItems } from './exceptions_viewer_items'; import { EditExceptionFlyout } from '../edit_exception_flyout'; import { AddExceptionFlyout } from '../add_exception_flyout'; +import * as i18n from '../translations'; +import { useFindExceptionListReferences } from '../use_find_references'; const initialState: State = { filterOptions: { filter: '', tags: [] }, @@ -49,8 +51,6 @@ const initialState: State = { exceptionListTypeToEdit: null, totalEndpointItems: 0, totalDetectionsItems: 0, - showEndpointListsOnly: false, - showDetectionsListsOnly: false, }; interface ExceptionsViewerProps { @@ -59,8 +59,7 @@ interface ExceptionsViewerProps { ruleIndices: string[]; dataViewId?: string; exceptionListsMeta: ExceptionListIdentifiers[]; - availableListTypes: ExceptionListTypeEnum[]; - commentsAccordionId: string; + listType: ExceptionListTypeEnum; onRuleChange?: () => void; } @@ -70,8 +69,7 @@ const ExceptionsViewerComponent = ({ ruleIndices, dataViewId, exceptionListsMeta, - availableListTypes, - commentsAccordionId, + listType, onRuleChange, }: ExceptionsViewerProps): JSX.Element => { const { services } = useKibana(); @@ -103,23 +101,29 @@ const ExceptionsViewerComponent = ({ exceptionListTypeToEdit, totalEndpointItems, totalDetectionsItems, - showDetectionsListsOnly, - showEndpointListsOnly, }, dispatch, ] = useReducer(allExceptionItemsReducer(), { ...initialState }); const { deleteExceptionItem, getExceptionListsItems } = useApi(services.http); - const [supportedListTypes, setSupportedListTypes] = useState([]); + const [isReadOnly, setReadOnly] = useState(true); + const [lastUpdated, setLastUpdated] = useState(null); + + // With upcoming redesign for 8.5, you'll probably have all the lists already and won't + // need to fetch the rule + const { rule: maybeRule } = useRuleAsync(ruleId); + const [isLoadingReferences, allReferences] = useFindExceptionListReferences( + maybeRule != null ? maybeRule?.exceptions_list ?? [] : [] + ); const [{ canUserCRUD, hasIndexWrite }] = useUserData(); useEffect((): void => { if (!canUserCRUD || !hasIndexWrite) { - setSupportedListTypes([]); + setReadOnly(true); } else { - setSupportedListTypes(availableListTypes); + setReadOnly(false); } - }, [availableListTypes, canUserCRUD, hasIndexWrite]); + }, [setReadOnly, canUserCRUD, hasIndexWrite]); const setExceptions = useCallback( ({ @@ -145,8 +149,8 @@ const ExceptionsViewerComponent = ({ perPage: pagination.pageSize, total: pagination.totalItemCount, }, - showDetectionsListsOnly, - showEndpointListsOnly, + showDetectionsListsOnly: listType !== ExceptionListTypeEnum.ENDPOINT, + showEndpointListsOnly: listType === ExceptionListTypeEnum.ENDPOINT, matchFilters: true, onSuccess: setExceptions, onError: onDispatchToaster({ @@ -178,53 +182,62 @@ const ExceptionsViewerComponent = ({ ); const handleGetTotals = useCallback(async (): Promise => { - await getExceptionListsItems({ - lists: exceptionListsMeta, - filterOptions: [], - pagination: { - page: 0, - perPage: 1, - total: 0, - }, - showDetectionsListsOnly: true, - showEndpointListsOnly: false, - onSuccess: ({ pagination: detectionPagination }) => { - setExceptionItemTotals(null, detectionPagination.total ?? 0); - }, - onError: () => { - const dispatchToasterError = onDispatchToaster({ - color: 'danger', - title: i18n.TOTAL_ITEMS_FETCH_ERROR, - iconType: 'alert', - }); + if (listType !== ExceptionListTypeEnum.ENDPOINT) { + await getExceptionListsItems({ + lists: exceptionListsMeta, + filterOptions: [], + pagination: { + page: 0, + perPage: 1, + total: 0, + }, + showDetectionsListsOnly: true, + showEndpointListsOnly: false, + onSuccess: ({ pagination: detectionPagination }) => { + setExceptionItemTotals(null, detectionPagination.total ?? 0); + }, + onError: () => { + const dispatchToasterError = onDispatchToaster({ + color: 'danger', + title: i18n.TOTAL_ITEMS_FETCH_ERROR, + iconType: 'alert', + }); - dispatchToasterError(); - }, - }); - await getExceptionListsItems({ - lists: exceptionListsMeta, - filterOptions: [], - pagination: { - page: 0, - perPage: 1, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: true, - onSuccess: ({ pagination: endpointPagination }) => { - setExceptionItemTotals(endpointPagination.total ?? 0, null); - }, - onError: () => { - const dispatchToasterError = onDispatchToaster({ - color: 'danger', - title: i18n.TOTAL_ITEMS_FETCH_ERROR, - iconType: 'alert', - }); + dispatchToasterError(); + }, + }); + } else if (listType === ExceptionListTypeEnum.ENDPOINT) { + await getExceptionListsItems({ + lists: exceptionListsMeta, + filterOptions: [], + pagination: { + page: 0, + perPage: 1, + total: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: true, + onSuccess: ({ pagination: endpointPagination }) => { + setExceptionItemTotals(endpointPagination.total ?? 0, null); + }, + onError: () => { + const dispatchToasterError = onDispatchToaster({ + color: 'danger', + title: i18n.TOTAL_ITEMS_FETCH_ERROR, + iconType: 'alert', + }); - dispatchToasterError(); - }, - }); - }, [setExceptionItemTotals, exceptionListsMeta, getExceptionListsItems, onDispatchToaster]); + dispatchToasterError(); + }, + }); + } + }, [ + listType, + getExceptionListsItems, + exceptionListsMeta, + setExceptionItemTotals, + onDispatchToaster, + ]); const handleFetchList = useCallback((): void => { if (fetchListItems != null) { @@ -243,16 +256,13 @@ const ExceptionsViewerComponent = ({ [dispatch] ); - const handleAddException = useCallback( - (type: ExceptionListTypeEnum): void => { - dispatch({ - type: 'updateExceptionListTypeToEdit', - exceptionListType: type, - }); - setCurrentModal('addException'); - }, - [setCurrentModal] - ); + const handleAddException = useCallback((): void => { + dispatch({ + type: 'updateExceptionListTypeToEdit', + exceptionListType: listType, + }); + setCurrentModal('addException'); + }, [listType, setCurrentModal]); const handleEditException = useCallback( (exception: ExceptionListItemSchema): void => { @@ -314,25 +324,27 @@ const ExceptionsViewerComponent = ({ // Logic for initial render useEffect((): void => { - if (isInitLoading && !loadingList && (exceptions.length === 0 || exceptions != null)) { + if ( + isInitLoading && + !isLoadingReferences && + !loadingList && + (exceptions.length === 0 || exceptions != null) + ) { handleGetTotals(); dispatch({ type: 'updateIsInitLoading', loading: false, }); - } - }, [handleGetTotals, isInitLoading, exceptions, loadingList, dispatch]); - // Used in utility bar info text - const ruleSettingsUrl = services.application.getUrlForApp( - `security/detections/rules/id/${encodeURI(ruleId)}/edit` - ); + setLastUpdated(Date.now()); + } + }, [handleGetTotals, isInitLoading, exceptions, loadingList, dispatch, isLoadingReferences]); const showEmpty: boolean = !isInitLoading && !loadingList && exceptions.length === 0; const showNoResults: boolean = exceptions.length === 0 && (totalEndpointItems > 0 || totalDetectionsItems > 0); - + console.log({ allReferences }); return ( <> {currentModal === 'editException' && @@ -364,46 +376,49 @@ const ExceptionsViewerComponent = ({ /> )} - - {(isInitLoading || loadingList) && ( + + {isInitLoading || loadingList || isLoadingReferences ? ( + ) : ( + <> + {!showEmpty && ( + <> + + + + + + + )} + + + + {!showEmpty && ( + + )} + )} - - - - - - - - - - - + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/no_exception_items.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/no_exception_items.tsx new file mode 100644 index 0000000000000..84f16e73324ae --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/no_exception_items.tsx @@ -0,0 +1,62 @@ +/* + * 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, { useCallback } from 'react'; +import { EuiFlexItem, EuiEmptyPrompt, EuiButton, useEuiTheme } from '@elastic/eui'; + +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import * as i18n from './translations'; + +interface ExeptionItemsViewerNoItemsComponentProps { + listType: ExceptionListTypeEnum; + onCreateExceptionListItem: () => void; +} + +const ExeptionItemsViewerNoItemsComponent = ({ + listType, + onCreateExceptionListItem, +}: ExeptionItemsViewerNoItemsComponentProps): JSX.Element => { + const { euiTheme } = useEuiTheme(); + const handleAddException = useCallback(() => { + onCreateExceptionListItem(); + }, [onCreateExceptionListItem]); + + return ( + + {i18n.EXCEPTION_EMPTY_PROMPT_TITLE} + } + body={ +

+ {listType === ExceptionListTypeEnum.ENDPOINT + ? i18n.EXCEPTION_EMPTY_ENDPOINT_PROMPT_BODY + : i18n.EXCEPTION_EMPTY_PROMPT_BODY} +

+ } + actions={ + + {listType === ExceptionListTypeEnum.ENDPOINT + ? i18n.EXCEPTION_EMPTY_PROMPT_ENDPOINT_BUTTON + : i18n.EXCEPTION_EMPTY_PROMPT_BUTTON} + + } + data-test-subj="exceptionsEmptyPrompt" + /> +
+ ); +}; + +ExeptionItemsViewerNoItemsComponent.displayName = 'ExeptionItemsViewerNoItemsComponent'; + +export const ExeptionItemsViewerNoItems = React.memo(ExeptionItemsViewerNoItemsComponent); + +ExeptionItemsViewerNoItems.displayName = 'ExeptionItemsViewerNoItems'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/no_search_results.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/no_search_results.tsx new file mode 100644 index 0000000000000..448bbcd30b23f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/no_search_results.tsx @@ -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 React from 'react'; +import { EuiFlexItem, EuiEmptyPrompt } from '@elastic/eui'; + +import * as i18n from './translations'; + +const ExeptionItemsViewerEmptySearchResultsComponent = (): JSX.Element => ( + + + {i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY} + + } + data-test-subj="exceptionItemsNoSearchResultsPrompt" + /> + +); + +ExeptionItemsViewerEmptySearchResultsComponent.displayName = + 'ExeptionItemsViewerEmptySearchResultsComponent'; + +export const ExeptionItemsViewerEmptySearchResults = React.memo( + ExeptionItemsViewerEmptySearchResultsComponent +); + +ExeptionItemsViewerEmptySearchResults.displayName = 'ExeptionItemsViewerEmptySearchResults'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts index 1d91f45fd0c9f..0caf7e9cd384f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts @@ -31,8 +31,6 @@ export interface State { exceptionListTypeToEdit: ExceptionListType | null; totalEndpointItems: number; totalDetectionsItems: number; - showEndpointListsOnly: boolean; - showDetectionsListsOnly: boolean; } export type Action = @@ -80,8 +78,7 @@ export const allExceptionItemsReducer = }; } case 'updateFilterOptions': { - const { filter, pagination, showEndpointListsOnly, showDetectionsListsOnly } = - action.filters; + const { filter, pagination } = action.filters; return { ...state, filterOptions: { @@ -92,8 +89,6 @@ export const allExceptionItemsReducer = ...state.pagination, ...pagination, }, - showEndpointListsOnly: showEndpointListsOnly ?? state.showEndpointListsOnly, - showDetectionsListsOnly: showDetectionsListsOnly ?? state.showDetectionsListsOnly, }; } case 'setExceptionItemTotals': { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/translations.ts new file mode 100644 index 0000000000000..4d5cc740dcd68 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/translations.ts @@ -0,0 +1,85 @@ +/* + * 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 EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody', + { + defaultMessage: 'No search results found', + } +); + +export const EXCEPTION_EMPTY_PROMPT_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.addExceptionsEmptyPromptTitle', + { + defaultMessage: 'Add exceptions to this rule', + } +); + +export const EXCEPTION_EMPTY_PROMPT_BODY = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.emptyPromptBody', + { + defaultMessage: 'There are no exceptions on your rule. Create your first rule exception.', + } +); + +export const EXCEPTION_EMPTY_ENDPOINT_PROMPT_BODY = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.endpoint.emptyPromptBody', + { + defaultMessage: + 'There are no endpoint exceptions. Endpoint exceptions are applied to the endpoint and the detection rule. Create your first endpoint exception.', + } +); + +export const EXCEPTION_EMPTY_PROMPT_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.emptyPromptButtonLabel', + { + defaultMessage: 'Add rule exception', + } +); + +export const EXCEPTION_EMPTY_PROMPT_ENDPOINT_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.endpoint.emptyPromptButtonLabel', + { + defaultMessage: 'Add endpoint exception', + } +); + +export const EXCEPTION_LOADING_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.loadingExceptionsTitle', + { + defaultMessage: 'Loading exceptions', + } +); + +export const CREATE_RULE_EXCEPTION_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.createRuleExceptionButtonLabel', + { + defaultMessage: 'Create rule exception', + } +); + +export const EXCEPTION_ITEMS_SEARCH_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.exceptionItemsSearchPlaceholder', + { + defaultMessage: 'Search on the fields below: name, description, value', + } +); + +export const exceptionItemsListFilterLabel = (numberLists: number) => + i18n.translate('xpack.securitySolution.exceptions.viewer.searchFilterLabel', { + values: { numberLists }, + defaultMessage: 'Exception {numberLists, plural, =1 {list} other {lists}} [{numberLists}]', + }); + +export const EXCEPTION_LISTS_FETCH_ERROR_TOASTER = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.exceptionListsFetchErrorDescription', + { + defaultMessage: 'Error fetching exception lists', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index dfee0f418d2d5..9a24eb8579882 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -371,3 +371,29 @@ export const fetchInstalledIntegrations = async ({ signal, } ); + +/** + * Fetch a Rule by providing a Rule ID + * + * @param id Rule ID's (not rule_id) + * @param http Kibana http service + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const findRuleExceptionReferences = async ({ + lists, + http, + signal, +}: unknown & { http: HttpStart }): Promise => { + console.log({ LISTS: lists }) + return http.fetch(`${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, { + method: 'GET', + query: { + list_ids: lists.map((l) => l.id).join(','), + list_list_ids: lists.map((l) => l.listId).join(','), + namespace_types: lists.map((l) => l.type).join(','), + }, + signal, + }); +} \ No newline at end of file diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 051e3db28dd7e..a1c9c04025877 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -146,37 +146,11 @@ const StyledMinHeightTabContainer = styled.div` export enum RuleDetailTabs { alerts = 'alerts', exceptions = 'exceptions', + endpointExceptions = 'endpointExceptions', executionResults = 'executionResults', executionEvents = 'executionEvents', } -const ruleDetailTabs = [ - { - id: RuleDetailTabs.alerts, - name: detectionI18n.ALERT, - disabled: false, - dataTestSubj: 'alertsTab', - }, - { - id: RuleDetailTabs.exceptions, - name: i18n.EXCEPTIONS_TAB, - disabled: false, - dataTestSubj: 'exceptionsTab', - }, - { - id: RuleDetailTabs.executionResults, - name: i18n.EXECUTION_RESULTS_TAB, - disabled: false, - dataTestSubj: 'executionResultsTab', - }, - { - id: RuleDetailTabs.executionEvents, - name: i18n.EXECUTION_EVENTS_TAB, - disabled: false, - dataTestSubj: 'executionEventsTab', - }, -]; - type DetectionEngineComponentProps = PropsFromRedux; const RuleDetailsPageComponent: React.FC = ({ @@ -238,7 +212,6 @@ const RuleDetailsPageComponent: React.FC = ({ runtimeMappings, loading: isLoadingIndexPattern, } = useSourcererDataView(SourcererScopeName.detections); - const loading = userInfoLoading || listsConfigLoading; const { detailName: ruleId } = useParams<{ detailName: string }>(); const { @@ -250,6 +223,79 @@ const RuleDetailsPageComponent: React.FC = ({ const { pollForSignalIndex } = useSignalHelpers(); const [rule, setRule] = useState(null); const isLoading = ruleLoading && rule == null; + + const exceptionLists = useMemo((): { + lists: ExceptionListIdentifiers[]; + allowedExceptionListTypes: ExceptionListTypeEnum[]; + } => { + if (rule != null && rule.exceptions_list != null) { + return rule.exceptions_list.reduce<{ + lists: ExceptionListIdentifiers[]; + allowedExceptionListTypes: ExceptionListTypeEnum[]; + }>( + (acc, { id, list_id: listId, namespace_type: namespaceType, type }) => { + const { allowedExceptionListTypes, lists } = acc; + const shouldAddEndpoint = + type === ExceptionListTypeEnum.ENDPOINT && + !allowedExceptionListTypes.includes(ExceptionListTypeEnum.ENDPOINT); + return { + lists: [...lists, { id, listId, namespaceType, type }], + allowedExceptionListTypes: shouldAddEndpoint + ? [...allowedExceptionListTypes, ExceptionListTypeEnum.ENDPOINT] + : allowedExceptionListTypes, + }; + }, + { lists: [], allowedExceptionListTypes: [ExceptionListTypeEnum.DETECTION] } + ); + } else { + return { lists: [], allowedExceptionListTypes: [ExceptionListTypeEnum.DETECTION] }; + } + }, [rule]); + + const ruleDetailTabsDefault = useMemo( + () => [ + { + id: RuleDetailTabs.alerts, + name: detectionI18n.ALERT, + disabled: false, + dataTestSubj: 'alertsTab', + }, + { + id: RuleDetailTabs.executionResults, + name: i18n.EXECUTION_RESULTS_TAB, + disabled: false, + dataTestSubj: 'executionResultsTab', + }, + { + id: RuleDetailTabs.executionEvents, + name: i18n.EXECUTION_EVENTS_TAB, + disabled: false, + dataTestSubj: 'executionEventsTab', + }, + { + id: RuleDetailTabs.exceptions, + name: i18n.EXCEPTIONS_TAB, + disabled: false, + dataTestSubj: 'exceptionsTab', + }, + ], + [] + ); + + const ruleDetailTabs = useMemo(() => { + return exceptionLists.allowedExceptionListTypes.includes(ExceptionListTypeEnum.ENDPOINT) + ? [ + ...ruleDetailTabsDefault, + { + id: RuleDetailTabs.endpointExceptions, + name: i18n.ENDPOINT_EXCEPTIONS_TAB, + disabled: false, + dataTestSubj: 'endpointExceptionsTab', + }, + ] + : ruleDetailTabsDefault; + }, [ruleDetailTabsDefault, exceptionLists]); + const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.alerts); const [pageTabs, setTabs] = useState(ruleDetailTabs); const { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData } = @@ -614,34 +660,6 @@ const RuleDetailsPageComponent: React.FC = ({ [setShowOnlyThreatIndicatorAlerts] ); - const exceptionLists = useMemo((): { - lists: ExceptionListIdentifiers[]; - allowedExceptionListTypes: ExceptionListTypeEnum[]; - } => { - if (rule != null && rule.exceptions_list != null) { - return rule.exceptions_list.reduce<{ - lists: ExceptionListIdentifiers[]; - allowedExceptionListTypes: ExceptionListTypeEnum[]; - }>( - (acc, { id, list_id: listId, namespace_type: namespaceType, type }) => { - const { allowedExceptionListTypes, lists } = acc; - const shouldAddEndpoint = - type === ExceptionListTypeEnum.ENDPOINT && - !allowedExceptionListTypes.includes(ExceptionListTypeEnum.ENDPOINT); - return { - lists: [...lists, { id, listId, namespaceType, type }], - allowedExceptionListTypes: shouldAddEndpoint - ? [...allowedExceptionListTypes, ExceptionListTypeEnum.ENDPOINT] - : allowedExceptionListTypes, - }; - }, - { lists: [], allowedExceptionListTypes: [ExceptionListTypeEnum.DETECTION] } - ); - } else { - return { lists: [], allowedExceptionListTypes: [ExceptionListTypeEnum.DETECTION] }; - } - }, [rule]); - const onSkipFocusBeforeEventsTable = useCallback(() => { focusUtilityBarAction(containerElement.current); }, [containerElement]); @@ -868,8 +886,18 @@ const RuleDetailsPageComponent: React.FC = ({ ruleName={rule?.name ?? ''} ruleIndices={rule?.index ?? DEFAULT_INDEX_PATTERN} dataViewId={rule?.data_view_id} - availableListTypes={exceptionLists.allowedExceptionListTypes} - commentsAccordionId={'ruleDetailsTabExceptions'} + listType={ExceptionListTypeEnum.DETECTION} + exceptionListsMeta={exceptionLists.lists} + onRuleChange={refreshRule} + /> + )} + {ruleDetailTab === RuleDetailTabs.endpointExceptions && ( + diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts index 299c355ffa480..1a50d570ea179 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts @@ -36,9 +36,16 @@ export const UNKNOWN = i18n.translate( ); export const EXCEPTIONS_TAB = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab', + 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExceptionsTab', { - defaultMessage: 'Exceptions', + defaultMessage: 'Rule exceptions', + } +); + +export const ENDPOINT_EXCEPTIONS_TAB = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.endpointExceptionsTab', + { + defaultMessage: 'Endpoint exceptions', } ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts new file mode 100644 index 0000000000000..ab082af8363df --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts @@ -0,0 +1,95 @@ +/* + * 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 { transformError } from '@kbn/securitysolution-es-utils'; +import type { Logger } from '@kbn/core/server'; + +import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { buildSiemResponse } from '../utils'; +import { enrichFilterWithRuleTypeMapping } from '../../rules/enrich_filter_with_rule_type_mappings'; +import type { FindExceptionReferencesOnRuleSchema } from '../../../../../common/detection_engine/schemas/request/find_exception_references_schema'; +import { findExceptionReferencesOnRuleSchema } from '../../../../../common/detection_engine/schemas/request/find_exception_references_schema'; +import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; +import { getSavedObjectType, getSavedObjectTypes } from '@kbn/securitysolution-list-utils'; + +export const findRuleExceptionReferencesRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.get( + { + path: `${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, + validate: { + query: buildRouteValidation< + typeof findExceptionReferencesOnRuleSchema, + FindExceptionReferencesOnRuleSchema + >(findExceptionReferencesOnRuleSchema), + }, + options: { + tags: ['access:securitySolution'], + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const { list_ids, namespace_types, list_list_ids } = request.query; + const ctx = await context.resolve(['core', 'securitySolution', 'alerting']); + const rulesClient = ctx.alerting.getRulesClient(); + + const results = await Promise.all( + list_ids.map(async (id, index) => { + return rulesClient.find({ + options: { + perPage: 1000, + filter: enrichFilterWithRuleTypeMapping(null), + hasReference: { + id, + type: getSavedObjectType({ namespaceType: namespace_types[index]}), + }, + }, + }); + }) + ); + + const a = results.reduce((acc, data, index) => { + const wantedData = data.data.map(({ name, id, params}) => ({ + name, + id, + ruleId: params.ruleId, + exceptionLists: params.exceptionsList, + })); + acc[list_list_ids[index]] = wantedData; + + return acc; + }, {}); + console.log({ RESULT: JSON.stringify(a) }); + return response.ok({ body: a ?? {} }); + // const ruleIds = rules.data.map((rule) => rule.id); + + // const [ruleExecutionSummaries, ruleActions] = await Promise.all([ + // ruleExecutionLog.getExecutionSummariesBulk(ruleIds), + // legacyGetBulkRuleActionsSavedObject({ alertIds: ruleIds, savedObjectsClient, logger }), + // ]); + + // const transformed = transformFindAlerts(rules, ruleExecutionSummaries, ruleActions); + // if (transformed == null) { + // return siemResponse.error({ statusCode: 500, body: 'Internal error transforming' }); + // } else { + // return response.ok({ body: transformed ?? {} }); + // } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 9161ee89cd52c..4ec57e8f5db8d 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -74,6 +74,7 @@ import { createPrebuiltSavedObjectsRoute } from '../lib/prebuilt_saved_objects/r import { readAlertsIndexExistsRoute } from '../lib/detection_engine/routes/index/read_alerts_index_exists_route'; import { getInstalledIntegrationsRoute } from '../lib/detection_engine/routes/fleet/get_installed_integrations/get_installed_integrations_route'; import { registerResolverRoutes } from '../endpoint/routes/resolver'; +import { findRuleExceptionReferencesRoute } from '../lib/detection_engine/routes/rules/find_rule_exceptions_route'; export const initRoutes = ( router: SecuritySolutionPluginRouter, @@ -130,6 +131,7 @@ export const initRoutes = ( patchTimelinesRoute(router, config, security); importRulesRoute(router, config, ml); exportRulesRoute(router, config, logger); + findRuleExceptionReferencesRoute(router, logger); importTimelinesRoute(router, config, security); exportTimelinesRoute(router, config, security); From 091cbab5a9479ac52ae332796a33ab2c0fd3cdbd Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 12 Aug 2022 21:07:07 +0000 Subject: [PATCH 02/30] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../common/components/exceptions/use_find_references.tsx | 4 ++-- .../detections/containers/detection_engine/rules/api.ts | 4 ++-- .../routes/rules/find_rule_exceptions_route.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_find_references.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_find_references.tsx index 444d3d0ea64e5..ff8995c300659 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_find_references.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_find_references.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { useEffect, useState, useCallback } from 'react'; -import type { ListArray, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; +import { useEffect, useState } from 'react'; +import type { ListArray } from '@kbn/securitysolution-io-ts-list-types'; import { findRuleExceptionReferences } from '../../../detections/containers/detection_engine/rules/api'; import { useKibana } from '../../lib/kibana'; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 9a24eb8579882..5670346a55cc8 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -386,7 +386,7 @@ export const findRuleExceptionReferences = async ({ http, signal, }: unknown & { http: HttpStart }): Promise => { - console.log({ LISTS: lists }) + console.log({ LISTS: lists }); return http.fetch(`${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, { method: 'GET', query: { @@ -396,4 +396,4 @@ export const findRuleExceptionReferences = async ({ }, signal, }); -} \ No newline at end of file +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts index ab082af8363df..045ded1b16f9c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts @@ -8,6 +8,7 @@ import { transformError } from '@kbn/securitysolution-es-utils'; import type { Logger } from '@kbn/core/server'; +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { buildSiemResponse } from '../utils'; @@ -15,7 +16,6 @@ import { enrichFilterWithRuleTypeMapping } from '../../rules/enrich_filter_with_ import type { FindExceptionReferencesOnRuleSchema } from '../../../../../common/detection_engine/schemas/request/find_exception_references_schema'; import { findExceptionReferencesOnRuleSchema } from '../../../../../common/detection_engine/schemas/request/find_exception_references_schema'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -import { getSavedObjectType, getSavedObjectTypes } from '@kbn/securitysolution-list-utils'; export const findRuleExceptionReferencesRoute = ( router: SecuritySolutionPluginRouter, @@ -50,7 +50,7 @@ export const findRuleExceptionReferencesRoute = ( filter: enrichFilterWithRuleTypeMapping(null), hasReference: { id, - type: getSavedObjectType({ namespaceType: namespace_types[index]}), + type: getSavedObjectType({ namespaceType: namespace_types[index] }), }, }, }); @@ -58,7 +58,7 @@ export const findRuleExceptionReferencesRoute = ( ); const a = results.reduce((acc, data, index) => { - const wantedData = data.data.map(({ name, id, params}) => ({ + const wantedData = data.data.map(({ name, id, params }) => ({ name, id, ruleId: params.ruleId, From 2489fda4ceb37276615bad787fff66b6a438be2b Mon Sep 17 00:00:00 2001 From: yctercero Date: Sun, 14 Aug 2022 14:34:30 -0700 Subject: [PATCH 03/30] continued cleanup, updated jest tests, added some tests --- ...d_exception_list_references_schema.test.ts | 90 +++++++ ... find_exception_list_references_schema.ts} | 11 +- ...d_exception_list_references_schema.test.ts | 169 +++++++++++++ .../find_exception_list_references_schema.ts | 37 +++ .../schemas/response/index.ts | 1 + .../components/exceptions/translations.ts | 97 +------- .../exceptions/use_find_references.tsx | 54 +++-- .../exception_item_card_conditions.tsx | 19 +- .../exception_item_card_meta.test.tsx | 133 +++++++++++ .../exception_item_card_meta.tsx | 53 ++--- .../viewer/exception_item_card/index.test.tsx | 169 +++++++++++-- .../viewer/exception_item_card/index.tsx | 5 +- .../viewer/exceptions_utility.test.tsx | 131 +--------- .../exceptions/viewer/exceptions_utility.tsx | 4 +- .../viewer/exceptions_viewer_header.test.tsx | 223 ++---------------- .../viewer/exceptions_viewer_header.tsx | 27 ++- .../viewer/exceptions_viewer_items.test.tsx | 32 +-- .../viewer/exceptions_viewer_items.tsx | 7 +- .../exceptions/viewer/index.test.tsx | 54 +++-- .../components/exceptions/viewer/index.tsx | 38 ++- .../exceptions/viewer/translations.ts | 27 --- .../containers/detection_engine/rules/api.ts | 18 +- .../detection_engine/rules/types.ts | 14 ++ .../rules/details/index.test.tsx | 62 +++++ .../detection_engine/rules/details/index.tsx | 20 +- .../rules/find_rule_exceptions_route.test.ts | 154 ++++++++++++ .../rules/find_rule_exceptions_route.ts | 67 +++--- .../group1/find_rule_exception_references.ts | 184 +++++++++++++++ .../security_and_spaces/group1/index.ts | 1 + 29 files changed, 1252 insertions(+), 649 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_list_references_schema.test.ts rename x-pack/plugins/security_solution/common/detection_engine/schemas/request/{find_exception_references_schema.ts => find_exception_list_references_schema.ts} (66%) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_exception_list_references_schema.test.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_exception_list_references_schema.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.test.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rule_exception_references.ts diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_list_references_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_list_references_schema.test.ts new file mode 100644 index 0000000000000..425928d8fa208 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_list_references_schema.test.ts @@ -0,0 +1,90 @@ +/* + * 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 { exactCheck, formatErrors, foldLeftRight } from '@kbn/securitysolution-io-ts-utils'; +import { findExceptionReferencesOnRuleSchema } from './find_exception_list_references_schema'; +import type { FindExceptionReferencesOnRuleSchema } from './find_exception_list_references_schema'; + +describe('find_exception_list_references_schema', () => { + test('validates all fields', () => { + const payload: FindExceptionReferencesOnRuleSchema = { + ids: 'abc,def', + list_ids: '123,456', + namespace_types: 'single,agnostic', + }; + + const decoded = findExceptionReferencesOnRuleSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual({ + ids: ['abc', 'def'], + list_ids: ['123', '456'], + namespace_types: ['single', 'agnostic'], + }); + }); + + test('"ids" cannot be undefined', () => { + const payload: Omit = { + list_ids: '123,456', + namespace_types: 'single,agnostic', + }; + + const decoded = findExceptionReferencesOnRuleSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['Invalid value "undefined" supplied to "ids"']); + expect(output.schema).toEqual({}); + }); + + test('"list_ids" cannot be undefined', () => { + const payload: Omit = { + ids: 'abc', + namespace_types: 'single', + }; + + const decoded = findExceptionReferencesOnRuleSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([ + 'Invalid value "undefined" supplied to "list_ids"', + ]); + expect(output.schema).toEqual({}); + }); + + test('defaults "namespacetypes" to ["single"] if none set', () => { + const payload: Omit = { + ids: 'abc', + list_ids: '123', + }; + + const decoded = findExceptionReferencesOnRuleSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual({ + ids: ['abc'], + list_ids: ['123'], + namespace_types: ['single'], + }); + }); + + test('cannot add extra values', () => { + const payload: FindExceptionReferencesOnRuleSchema & { extra_value?: string } = { + ids: 'abc,def', + list_ids: '123,456', + namespace_types: 'single,agnostic', + extra_value: 'aaa', + }; + + const decoded = findExceptionReferencesOnRuleSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['invalid keys "extra_value"']); + expect(output.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_references_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_list_references_schema.ts similarity index 66% rename from x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_references_schema.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_list_references_schema.ts index e4e23c86344ac..8cd21df8ab08b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_references_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_list_references_schema.ts @@ -7,15 +7,20 @@ import * as t from 'io-ts'; import { NonEmptyStringArray } from '@kbn/securitysolution-io-ts-types'; +import { DefaultNamespaceArray } from '@kbn/securitysolution-io-ts-list-types'; export const findExceptionReferencesOnRuleSchema = t.exact( - t.partial({ + t.type({ + ids: NonEmptyStringArray, list_ids: NonEmptyStringArray, - list_list_ids: NonEmptyStringArray, - namespace_types: NonEmptyStringArray, + namespace_types: DefaultNamespaceArray, }) ); export type FindExceptionReferencesOnRuleSchema = t.OutputOf< typeof findExceptionReferencesOnRuleSchema >; + +export type FindExceptionReferencesOnRuleSchemaDecoded = t.TypeOf< + typeof findExceptionReferencesOnRuleSchema +>; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_exception_list_references_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_exception_list_references_schema.test.ts new file mode 100644 index 0000000000000..743645ba0f818 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_exception_list_references_schema.test.ts @@ -0,0 +1,169 @@ +/* + * 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 { exactCheck, formatErrors, foldLeftRight } from '@kbn/securitysolution-io-ts-utils'; +import { + ruleReferenceSchema, + rulesReferencedByExceptionListsSchema, +} from './find_exception_list_references_schema'; +import type { + RuleReferenceSchema, + RulesReferencedByExceptionListsSchema, +} from './find_exception_list_references_schema'; + +describe('find_exception_list_references_schema', () => { + describe('ruleReferenceSchema', () => { + test('validates all fields', () => { + const payload: RuleReferenceSchema = { + name: 'My rule', + id: '4656dc92-5832-11ea-8e2d-0242ac130003', + rule_id: 'my-rule-id', + exception_lists: [ + { + id: 'myListId', + list_id: 'my-list-id', + namespace_type: 'single', + type: 'detection', + }, + ], + }; + + const decoded = ruleReferenceSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual({ + exception_lists: [ + { id: 'myListId', list_id: 'my-list-id', namespace_type: 'single', type: 'detection' }, + ], + id: '4656dc92-5832-11ea-8e2d-0242ac130003', + name: 'My rule', + rule_id: 'my-rule-id', + }); + }); + + test('cannot add extra values', () => { + const payload: RuleReferenceSchema & { extra_value?: string } = { + name: 'My rule', + id: '4656dc92-5832-11ea-8e2d-0242ac130003', + rule_id: 'my-rule-id', + extra_value: 'foo', + exception_lists: [ + { + id: 'myListId', + list_id: 'my-list-id', + namespace_type: 'single', + type: 'detection', + }, + ], + }; + + const decoded = ruleReferenceSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['invalid keys "extra_value"']); + expect(output.schema).toEqual({}); + }); + }); + + describe('rulesReferencedByExceptionListsSchema', () => { + test('validates all fields', () => { + const payload: RulesReferencedByExceptionListsSchema = { + references: [ + { + 'my-list-id': [ + { + name: 'My rule', + id: '4656dc92-5832-11ea-8e2d-0242ac130003', + rule_id: 'my-rule-id', + exception_lists: [ + { + id: 'myListId', + list_id: 'my-list-id', + namespace_type: 'single', + type: 'detection', + }, + ], + }, + ], + }, + ], + }; + + const decoded = rulesReferencedByExceptionListsSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual({ + references: [ + { + 'my-list-id': [ + { + exception_lists: [ + { + id: 'myListId', + list_id: 'my-list-id', + namespace_type: 'single', + type: 'detection', + }, + ], + id: '4656dc92-5832-11ea-8e2d-0242ac130003', + name: 'My rule', + rule_id: 'my-rule-id', + }, + ], + }, + ], + }); + }); + + test('validates "references" with empty array', () => { + const payload: RulesReferencedByExceptionListsSchema = { + references: [], + }; + + const decoded = rulesReferencedByExceptionListsSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual({ + references: [], + }); + }); + + test('cannot add extra values', () => { + const payload: RulesReferencedByExceptionListsSchema & { extra_value?: string } = { + extra_value: 'foo', + references: [ + { + 'my-list-id': [ + { + name: 'My rule', + id: '4656dc92-5832-11ea-8e2d-0242ac130003', + rule_id: 'my-rule-id', + exception_lists: [ + { + id: 'myListId', + list_id: 'my-list-id', + namespace_type: 'single', + type: 'detection', + }, + ], + }, + ], + }, + ], + }; + + const decoded = rulesReferencedByExceptionListsSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['invalid keys "extra_value"']); + expect(output.schema).toEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_exception_list_references_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_exception_list_references_schema.ts new file mode 100644 index 0000000000000..a7f2527edc096 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_exception_list_references_schema.ts @@ -0,0 +1,37 @@ +/* + * 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 * as t from 'io-ts'; + +import { listArray, list_id } from '@kbn/securitysolution-io-ts-list-types'; + +import { rule_id, id, name } from '../common/schemas'; + +export const ruleReferenceSchema = t.exact( + t.type({ + name, + id, + rule_id, + exception_lists: listArray, + }) +); + +export type RuleReferenceSchema = t.OutputOf; + +export const rulesReferencedByExceptionListSchema = t.record(list_id, t.array(ruleReferenceSchema)); + +export type RuleReferencesSchema = t.OutputOf; + +export const rulesReferencedByExceptionListsSchema = t.exact( + t.type({ + references: t.array(rulesReferencedByExceptionListSchema), + }) +); + +export type RulesReferencedByExceptionListsSchema = t.OutputOf< + typeof rulesReferencedByExceptionListsSchema +>; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts index 1b688ce641a7a..e12fbf2918302 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts @@ -7,6 +7,7 @@ export * from './error_schema'; export * from './get_installed_integrations_response_schema'; +export * from './find_exception_list_references_schema'; export * from './import_rules_schema'; export * from './prepackaged_rules_schema'; export * from './prepackaged_rules_status_schema'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts index d9b6fc8739b90..6d05c70d288d2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts @@ -7,25 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const DETECTION_LIST = i18n.translate( - 'xpack.securitySolution.exceptions.detectionListLabel', - { - defaultMessage: 'Detection list', - } -); - -export const ENDPOINT_LIST = i18n.translate('xpack.securitySolution.exceptions.endpointListLabel', { - defaultMessage: 'Endpoint list', -}); - -export const EDIT = i18n.translate('xpack.securitySolution.exceptions.editButtonLabel', { - defaultMessage: 'Edit', -}); - -export const REMOVE = i18n.translate('xpack.securitySolution.exceptions.removeButtonLabel', { - defaultMessage: 'Remove', -}); - export const COMMENTS_SHOW = (comments: number) => i18n.translate('xpack.securitySolution.exceptions.showCommentsLabel', { values: { comments }, @@ -38,14 +19,6 @@ export const COMMENTS_HIDE = (comments: number) => defaultMessage: 'Hide ({comments}) {comments, plural, =1 {Comment} other {Comments}}', }); -export const NAME = i18n.translate('xpack.securitySolution.exceptions.nameLabel', { - defaultMessage: 'Name', -}); - -export const COMMENT = i18n.translate('xpack.securitySolution.exceptions.commentLabel', { - defaultMessage: 'Comment', -}); - export const COMMENT_EVENT = i18n.translate('xpack.securitySolution.exceptions.commentEventLabel', { defaultMessage: 'added a comment', }); @@ -64,13 +37,6 @@ export const SEARCH_DEFAULT = i18n.translate( } ); -export const ADD_EXCEPTION_LABEL = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.addExceptionLabel', - { - defaultMessage: 'Add new exception', - } -); - export const ADD_TO_ENDPOINT_LIST = i18n.translate( 'xpack.securitySolution.exceptions.viewer.addToEndpointListLabel', { @@ -85,28 +51,6 @@ export const ADD_TO_DETECTIONS_LIST = i18n.translate( } ); -export const EXCEPTION_EMPTY_PROMPT_TITLE = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.emptyPromptTitle', - { - defaultMessage: 'This rule has no exceptions', - } -); - -export const EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody', - { - defaultMessage: 'No search results found.', - } -); - -export const EXCEPTION_EMPTY_PROMPT_BODY = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.emptyPromptBody', - { - defaultMessage: - 'You can add exceptions to fine tune the rule so that detection alerts are not created when exception conditions are met. Exceptions improve detection accuracy, which can help reduce the number of false positives.', - } -); - export const FETCH_LIST_ERROR = i18n.translate( 'xpack.securitySolution.exceptions.viewer.fetchingListError', { @@ -133,36 +77,6 @@ export const NUMBER_OF_ITEMS = (items: number) => defaultMessage: '{items} items', }); -export const REFRESH = i18n.translate('xpack.securitySolution.exceptions.utilityRefreshLabel', { - defaultMessage: 'Refresh', -}); - -export const SHOWING_EXCEPTIONS = (items: number) => - i18n.translate('xpack.securitySolution.exceptions.utilityNumberExceptionsLabel', { - values: { items }, - defaultMessage: 'Showing {items} {items, plural, =1 {exception} other {exceptions}}', - }); - -export const FIELD = i18n.translate('xpack.securitySolution.exceptions.fieldDescription', { - defaultMessage: 'Field', -}); - -export const OPERATOR = i18n.translate('xpack.securitySolution.exceptions.operatorDescription', { - defaultMessage: 'Operator', -}); - -export const VALUE = i18n.translate('xpack.securitySolution.exceptions.valueDescription', { - defaultMessage: 'Value', -}); - -export const AND = i18n.translate('xpack.securitySolution.exceptions.andDescription', { - defaultMessage: 'AND', -}); - -export const OR = i18n.translate('xpack.securitySolution.exceptions.orDescription', { - defaultMessage: 'OR', -}); - export const ADD_COMMENT_PLACEHOLDER = i18n.translate( 'xpack.securitySolution.exceptions.viewer.addCommentPlaceholder', { @@ -177,10 +91,6 @@ export const ADD_TO_CLIPBOARD = i18n.translate( } ); -export const DESCRIPTION = i18n.translate('xpack.securitySolution.exceptions.descriptionLabel', { - defaultMessage: 'Description', -}); - export const TOTAL_ITEMS_FETCH_ERROR = i18n.translate( 'xpack.securitySolution.exceptions.viewer.fetchTotalsError', { @@ -264,3 +174,10 @@ export const OPERATING_SYSTEM_LINUX = i18n.translate( defaultMessage: 'Linux', } ); + +export const ERROR_FETCHING_REFERENCES_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.fetchingReferencesErrorToastTitle', + { + defaultMessage: 'Error fetching exception references', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_find_references.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_find_references.tsx index 444d3d0ea64e5..65f0b37a274c0 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_find_references.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_find_references.tsx @@ -5,58 +5,76 @@ * 2.0. */ -import { useEffect, useState, useCallback } from 'react'; -import type { ListArray, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; +import { useEffect, useMemo, useState } from 'react'; +import type { ListArray } from '@kbn/securitysolution-io-ts-list-types'; +import type { RuleReferenceSchema } from '../../../../common/detection_engine/schemas/response'; import { findRuleExceptionReferences } from '../../../detections/containers/detection_engine/rules/api'; -import { useKibana } from '../../lib/kibana'; +import { useKibana, useToasts } from '../../lib/kibana'; +import type { FindRulesReferencedByExceptionsListProp } from '../../../detections/containers/detection_engine/rules/types'; +import * as i18n from './translations'; -export type ReturnUseFindExceptionListReferences = [boolean, unknown[]]; +export type ReturnUseFindExceptionListReferences = [boolean, RuleReferences | null]; +export interface RuleReferences { + [key: string]: RuleReferenceSchema[]; +} /** * Hook for finding what rules reference a set of exception lists - * @param listReferences static id of + * @param listReferences array of exception list info stored on a rule */ export const useFindExceptionListReferences = ( ruleExceptionLists: ListArray ): ReturnUseFindExceptionListReferences => { const { http } = useKibana().services; - const [isLoading, setIsLoading] = useState(true); - const [references, setReferences] = useState(null); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - const refs = ruleExceptionLists.map((list) => { + const toasts = useToasts(); + const [isLoading, setIsLoading] = useState(false); + const [references, setReferences] = useState(null); + const listRefs = useMemo((): FindRulesReferencedByExceptionsListProp[] => { + return ruleExceptionLists.map((list) => { return { id: list.id, listId: list.list_id, - type: list.namespace_type, + namespaceType: list.namespace_type, }; }); + }, [ruleExceptionLists]); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); const findReferences = async () => { try { setIsLoading(true); - const addOrUpdateItems = await findRuleExceptionReferences({ - lists: refs, + const { references: referencesResults } = await findRuleExceptionReferences({ + lists: listRefs, http, signal: abortCtrl.signal, }); + const results = referencesResults.reduce((acc, result) => { + const [[key, value]] = Object.entries(result); + + acc[key] = value; + + return acc; + }, {}); + if (isSubscribed) { setIsLoading(false); - setReferences(addOrUpdateItems); + setReferences(results); } } catch (error) { if (isSubscribed) { setIsLoading(false); + toasts.addError(error, { title: i18n.ERROR_FETCHING_REFERENCES_TITLE }); } } }; - if (refs.length === 0 && isSubscribed) { + if (listRefs.length === 0 && isSubscribed) { setIsLoading(false); setReferences(null); } else { @@ -67,7 +85,7 @@ export const useFindExceptionListReferences = ( isSubscribed = false; abortCtrl.abort(); }; - }, [http, ruleExceptionLists]); + }, [http, ruleExceptionLists, listRefs, toasts]); return [isLoading, references]; }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx index b84c3aef017ed..bb5bef86caa0c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx @@ -160,7 +160,12 @@ export const ExceptionItemCardConditions = memo( ); return ( -
+ {osLabel != null && (
@@ -176,13 +181,7 @@ export const ExceptionItemCardConditions = memo( const operator = 'operator' in entry ? entry.operator : ''; return ( - +
( />
{nestedEntries != null && getNestedEntriesContent(type, nestedEntries)} - +
); })} -
+
); } ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx index b5a24ef3e472d..abcae47cee74b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx @@ -18,6 +18,27 @@ describe('ExceptionItemCardMetaInfo', () => { @@ -36,6 +57,27 @@ describe('ExceptionItemCardMetaInfo', () => { @@ -48,4 +90,95 @@ describe('ExceptionItemCardMetaInfo', () => { wrapper.find('[data-test-subj="exceptionItemMeta-updatedBy-value2"]').at(0).text() ).toEqual('some user'); }); + + it('it renders references info', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemMeta-affectedRulesButton"]').at(0).text() + ).toEqual('Affects 1 rule'); + }); + + it('it renders references info when multiple references exist', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemMeta-affectedRulesButton"]').at(0).text() + ).toEqual('Affects 2 rules'); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx index b47a27a20665e..ba47fef0af2a1 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx @@ -28,6 +28,7 @@ import { APP_UI_ID, SecurityPageName } from '../../../../../../common/constants' import { useKibana } from '../../../../lib/kibana'; import { getRuleDetailsUrl } from '../../../link_to/redirect_to_detection_engine'; import { useGetSecuritySolutionUrl } from '../../../link_to'; +import type { RuleReferenceSchema } from '../../../../../../common/detection_engine/schemas/response'; const StyledFlexItem = styled(EuiFlexItem)` border-right: 1px solid #d3dae6; @@ -36,7 +37,7 @@ const StyledFlexItem = styled(EuiFlexItem)` export interface ExceptionItemCardMetaInfoProps { item: ExceptionListItemSchema; - references: unknown; + references: RuleReferenceSchema[]; dataTestSubj: string; } @@ -92,7 +93,7 @@ export const ExceptionItemCardMetaInfo = memo( } + value1={} value2={item.created_by} dataTestSubj={`${dataTestSubj}-createdBy`} /> @@ -101,32 +102,30 @@ export const ExceptionItemCardMetaInfo = memo( } + value1={} value2={item.updated_by} dataTestSubj={`${dataTestSubj}-updatedBy`} /> - {references != null && ( - - - {i18n.AFFECTED_RULES(references.length)} - - } - panelPaddingSize="none" - isOpen={isPopoverOpen} - closePopover={onClosePopover} - data-test-subj={`${dataTestSubj}-items`} - > - - - - )} + + + {i18n.AFFECTED_RULES(references.length)} + + } + panelPaddingSize="none" + isOpen={isPopoverOpen} + closePopover={onClosePopover} + data-test-subj={`${dataTestSubj}-items`} + > + + + ); } @@ -144,12 +143,12 @@ interface MetaInfoDetailsProps { const MetaInfoDetails = memo(({ label, value1, value2, dataTestSubj }) => { return ( - + {label} - + {value1} @@ -159,7 +158,7 @@ const MetaInfoDetails = memo(({ label, value1, value2, dat {i18n.EXCEPTION_ITEM_META_BY} - + diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.test.tsx index 46a0f74642c08..d7a92bff76fae 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.test.tsx @@ -6,40 +6,53 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; import { ExceptionItemCard } from '.'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; import { getCommentsArrayMock } from '@kbn/lists-plugin/common/schemas/types/comment.mock'; -import { getMockTheme } from '../../../../lib/kibana/kibana_react.mock'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import { TestProviders } from '../../../../mock'; jest.mock('../../../../lib/kibana'); -const mockTheme = getMockTheme({ - eui: { - euiColorDanger: '#ece', - euiColorLightestShade: '#ece', - euiColorPrimary: '#ece', - euiFontWeightSemiBold: 1, - }, -}); - describe('ExceptionItemCard', () => { it('it renders header, item meta information and conditions', () => { const exceptionItem = { ...getExceptionListItemSchemaMock(), comments: [] }; const wrapper = mount( - + - + ); expect(wrapper.find('ExceptionItemCardHeader')).toHaveLength(1); @@ -54,7 +67,7 @@ describe('ExceptionItemCard', () => { const exceptionItem = { ...getExceptionListItemSchemaMock(), comments: getCommentsArrayMock() }; const wrapper = mount( - + { onEditException={jest.fn()} exceptionItem={exceptionItem} dataTestSubj="item" + listType={ExceptionListTypeEnum.DETECTION} + ruleReferences={[ + { + exception_lists: [ + { + id: '123', + list_id: 'i_exist', + namespace_type: 'single', + type: 'detection', + }, + { + id: '456', + list_id: 'i_exist_2', + namespace_type: 'single', + type: 'detection', + }, + ], + id: '1a2b3c', + name: 'Simple Rule Query', + rule_id: 'rule-2', + }, + ]} /> - + ); expect(wrapper.find('ExceptionItemCardHeader')).toHaveLength(1); @@ -78,7 +113,7 @@ describe('ExceptionItemCard', () => { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - + { onEditException={jest.fn()} exceptionItem={exceptionItem} dataTestSubj="item" + listType={ExceptionListTypeEnum.DETECTION} + ruleReferences={[ + { + exception_lists: [ + { + id: '123', + list_id: 'i_exist', + namespace_type: 'single', + type: 'detection', + }, + { + id: '456', + list_id: 'i_exist_2', + namespace_type: 'single', + type: 'detection', + }, + ], + id: '1a2b3c', + name: 'Simple Rule Query', + rule_id: 'rule-2', + }, + ]} /> - + ); expect(wrapper.find('button[data-test-subj="item-actionButton"]').exists()).toBeFalsy(); @@ -98,7 +155,7 @@ describe('ExceptionItemCard', () => { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - + { onEditException={mockOnEditException} exceptionItem={exceptionItem} dataTestSubj="item" + listType={ExceptionListTypeEnum.DETECTION} + ruleReferences={[ + { + exception_lists: [ + { + id: '123', + list_id: 'i_exist', + namespace_type: 'single', + type: 'detection', + }, + { + id: '456', + list_id: 'i_exist_2', + namespace_type: 'single', + type: 'detection', + }, + ], + id: '1a2b3c', + name: 'Simple Rule Query', + rule_id: 'rule-2', + }, + ]} /> - + ); // click on popover @@ -127,7 +206,7 @@ describe('ExceptionItemCard', () => { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - + { onEditException={jest.fn()} exceptionItem={exceptionItem} dataTestSubj="item" + listType={ExceptionListTypeEnum.DETECTION} + ruleReferences={[ + { + exception_lists: [ + { + id: '123', + list_id: 'i_exist', + namespace_type: 'single', + type: 'detection', + }, + { + id: '456', + list_id: 'i_exist_2', + namespace_type: 'single', + type: 'detection', + }, + ], + id: '1a2b3c', + name: 'Simple Rule Query', + rule_id: 'rule-2', + }, + ]} /> - + ); // click on popover @@ -158,7 +259,7 @@ describe('ExceptionItemCard', () => { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( - + { onEditException={jest.fn()} exceptionItem={exceptionItem} dataTestSubj="item" + listType={ExceptionListTypeEnum.DETECTION} + ruleReferences={[ + { + exception_lists: [ + { + id: '123', + list_id: 'i_exist', + namespace_type: 'single', + type: 'detection', + }, + { + id: '456', + list_id: 'i_exist_2', + namespace_type: 'single', + type: 'detection', + }, + ], + id: '1a2b3c', + name: 'Simple Rule Query', + rule_id: 'rule-2', + }, + ]} /> - + ); expect(wrapper.find('.euiAccordion-isOpen')).toHaveLength(0); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx index a00ae6d4fd5df..10c71b47d9db5 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx @@ -25,16 +25,17 @@ import * as i18n from './translations'; import { ExceptionItemCardHeader } from './exception_item_card_header'; import { ExceptionItemCardConditions } from './exception_item_card_conditions'; import { ExceptionItemCardMetaInfo } from './exception_item_card_meta'; +import type { RuleReferenceSchema } from '../../../../../../common/detection_engine/schemas/response'; export interface ExceptionItemProps { loadingItemIds: ExceptionListItemIdentifiers[]; exceptionItem: ExceptionListItemSchema; listType: ExceptionListTypeEnum; + disableActions: boolean; + ruleReferences: RuleReferenceSchema[]; onDeleteException: (arg: ExceptionListItemIdentifiers) => void; onEditException: (item: ExceptionListItemSchema) => void; - disableActions: boolean; dataTestSubj: string; - ruleReferences: unknown; } const ExceptionItemCardComponent = ({ diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx index f600fe28f3ecb..49bb72764edb2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx @@ -6,26 +6,15 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { ExceptionsViewerUtility } from './exceptions_utility'; -import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; - -const mockTheme = getMockTheme({ - eui: { - euiBreakpoints: { - l: '1200px', - }, - euiSizeM: '10px', - euiBorderThin: '1px solid #ece', - }, -}); +import { TestProviders } from '../../../mock'; describe('ExceptionsViewerUtility', () => { - it('it renders correct pluralized text when more than one exception exists', () => { + it('it renders correct item counts', () => { const wrapper = mountWithIntl( - + { totalItemCount: 2, pageSizeOptions: [5, 10, 20, 50, 100], }} - showEndpointListsOnly={false} - showDetectionsListsOnly={false} - ruleSettingsUrl={'some/url'} - onRefreshClick={jest.fn()} - /> - - ); - - expect(wrapper.find('[data-test-subj="exceptionsShowing"]').at(0).text()).toEqual( - 'Showing 2 exceptions' - ); - }); - - it('it renders correct singular text when less than two exceptions exists', () => { - const wrapper = mountWithIntl( - - - + ); expect(wrapper.find('[data-test-subj="exceptionsShowing"]').at(0).text()).toEqual( - 'Showing 1 exception' - ); - }); - - it('it invokes "onRefreshClick" when refresh button clicked', () => { - const mockOnRefreshClick = jest.fn(); - const wrapper = mountWithIntl( - - - - ); - - wrapper.find('[data-test-subj="exceptionsRefresh"] button').simulate('click'); - - expect(mockOnRefreshClick).toHaveBeenCalledTimes(1); - }); - - it('it does not render any messages when "showEndpointList" and "showDetectionsList" are "false"', () => { - const wrapper = mountWithIntl( - - - + 'Showing 1-2 of 2' ); - - expect(wrapper.find('[data-test-subj="exceptionsEndpointMessage"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="exceptionsDetectionsMessage"]').exists()).toBeFalsy(); }); - it('it does render detections messages when "showDetectionsList" is "true"', () => { + it('it renders last updated message', () => { const wrapper = mountWithIntl( - + { totalItemCount: 1, pageSizeOptions: [5, 10, 20, 50, 100], }} - showEndpointListsOnly={false} - showDetectionsListsOnly - ruleSettingsUrl={'some/url'} - onRefreshClick={jest.fn()} + lastUpdated={Date.now()} /> - + ); - expect(wrapper.find('[data-test-subj="exceptionsEndpointMessage"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="exceptionsDetectionsMessage"]').exists()).toBeTruthy(); - }); - - it('it does render endpoint messages when "showEndpointList" is "true"', () => { - const wrapper = mountWithIntl( - - - + expect(wrapper.find('[data-test-subj="exceptionsViewerLastUpdated"]').at(0).text()).toEqual( + 'Updated now' ); - - expect(wrapper.find('[data-test-subj="exceptionsEndpointMessage"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="exceptionsDetectionsMessage"]').exists()).toBeFalsy(); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx index b025e54e97a51..7bc12b3a9b2f9 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx @@ -45,7 +45,6 @@ const ExceptionsViewerUtilityComponent: React.FC = {`1-${pagination.totalItemCount}`}, partTwo: {`${pagination.totalItemCount}`}, @@ -57,11 +56,10 @@ const ExceptionsViewerUtilityComponent: React.FC = - + diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx index d19a81e222423..ff5c9ff758604 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx @@ -16,10 +16,9 @@ describe('ExceptionsViewerHeader', () => { it('it renders all disabled if "isInitLoading" is true', () => { const wrapper = mount( @@ -28,177 +27,35 @@ describe('ExceptionsViewerHeader', () => { expect( wrapper.find('input[data-test-subj="exceptionsHeaderSearch"]').at(0).prop('disabled') ).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="exceptionsDetectionFilterBtn"] button').at(0).prop('disabled') - ).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="exceptionsEndpointFilterBtn"] button').at(0).prop('disabled') - ).toBeTruthy(); expect( wrapper - .find('[data-test-subj="exceptionsHeaderAddExceptionPopoverBtn"] button') + .find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button') .at(0) .prop('disabled') ).toBeTruthy(); }); - // This occurs if user does not have sufficient privileges - it('it does not display add exception button if no list types available', () => { + it('it does not display add exception button if user is read only', () => { const wrapper = mount( ); expect(wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"]').exists()).toBeFalsy(); }); - it('it displays toggles and add exception popover when more than one list type available', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="exceptionsFilterGroupBtns"]').exists()).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionPopoverBtn"]').exists() - ).toBeTruthy(); - }); - - it('it does not display toggles and add exception popover if only one list type is available', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="exceptionsFilterGroupBtns"]')).toHaveLength(0); - expect(wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionPopoverBtn"]')).toHaveLength( - 0 - ); - }); - - it('it displays add exception button without popover if only one list type is available', () => { - const wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"]').exists() - ).toBeTruthy(); - }); - - it('it renders detections filter toggle selected when clicked', () => { - const mockOnFilterChange = jest.fn(); - const wrapper = mount( - - ); - - wrapper.find('[data-test-subj="exceptionsDetectionFilterBtn"] button').simulate('click'); - - expect( - wrapper - .find('EuiFilterButton[data-test-subj="exceptionsDetectionFilterBtn"]') - .at(0) - .prop('hasActiveFilters') - ).toBeTruthy(); - expect( - wrapper - .find('EuiFilterButton[data-test-subj="exceptionsEndpointFilterBtn"]') - .at(0) - .prop('hasActiveFilters') - ).toBeFalsy(); - expect(mockOnFilterChange).toHaveBeenCalledWith({ - filter: { - filter: '', - tags: [], - }, - pagination: { - pageIndex: 0, - }, - showDetectionsListsOnly: true, - showEndpointListsOnly: false, - }); - }); - - it('it renders endpoint filter toggle selected and invokes "onFilterChange" when clicked', () => { - const mockOnFilterChange = jest.fn(); - const wrapper = mount( - - ); - - wrapper.find('[data-test-subj="exceptionsEndpointFilterBtn"] button').simulate('click'); - - expect( - wrapper - .find('EuiFilterButton[data-test-subj="exceptionsEndpointFilterBtn"]') - .at(0) - .prop('hasActiveFilters') - ).toBeTruthy(); - expect( - wrapper - .find('EuiFilterButton[data-test-subj="exceptionsDetectionFilterBtn"]') - .at(0) - .prop('hasActiveFilters') - ).toBeFalsy(); - expect(mockOnFilterChange).toHaveBeenCalledWith({ - filter: { - filter: '', - tags: [], - }, - pagination: { - pageIndex: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: true, - }); - }); - - it('it invokes "onAddExceptionClick" when user selects to add an exception item and only endpoint exception lists are available', () => { + it('it invokes "onAddExceptionClick" when user selects to add an exception item', () => { const mockOnAddExceptionClick = jest.fn(); const wrapper = mount( @@ -206,77 +63,39 @@ describe('ExceptionsViewerHeader', () => { wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button').simulate('click'); - expect(mockOnAddExceptionClick).toHaveBeenCalledTimes(1); - }); - - it('it invokes "onAddDetectionsExceptionClick" when user selects to add an exception item and only endpoint detections lists are available', () => { - const mockOnAddExceptionClick = jest.fn(); - const wrapper = mount( - + expect(wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"]').at(0).text()).toEqual( + 'Add rule exception' ); - - wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button').simulate('click'); - - expect(mockOnAddExceptionClick).toHaveBeenCalledTimes(1); + expect(mockOnAddExceptionClick).toHaveBeenCalledWith('detection'); }); - it('it invokes "onAddEndpointExceptionClick" when user selects to add an exception item to endpoint list from popover', () => { + it('it invokes "onAddExceptionClick" when user selects to add an endpoint exception item', () => { const mockOnAddExceptionClick = jest.fn(); const wrapper = mount( ); - wrapper - .find('[data-test-subj="exceptionsHeaderAddExceptionPopoverBtn"] button') - .simulate('click'); - wrapper.find('[data-test-subj="addEndpointExceptionBtn"] button').simulate('click'); - - expect(mockOnAddExceptionClick).toHaveBeenCalledTimes(1); - }); + wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button').simulate('click'); - it('it invokes "onAddDetectionsExceptionClick" when user selects to add an exception item to endpoint list from popover', () => { - const mockOnAddExceptionClick = jest.fn(); - const wrapper = mount( - + expect(wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"]').at(0).text()).toEqual( + 'Add endpoint exception' ); - - wrapper - .find('[data-test-subj="exceptionsHeaderAddExceptionPopoverBtn"] button') - .simulate('click'); - wrapper.find('[data-test-subj="addDetectionsExceptionBtn"] button').simulate('click'); - - expect(mockOnAddExceptionClick).toHaveBeenCalledTimes(1); + expect(mockOnAddExceptionClick).toHaveBeenCalledWith('endpoint'); }); it('it invokes "onFilterChange" when search used and "Enter" pressed', () => { const mockOnFilterChange = jest.fn(); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx index 8a350af5e9f1a..7d1a955d372cd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx @@ -13,6 +13,7 @@ import * as i18n from '../translations'; import type { Filter } from '../types'; interface ExceptionsViewerHeaderProps { + isReadOnly: boolean; isInitLoading: boolean; listType: ExceptionListTypeEnum; onFilterChange: (arg: Partial) => void; @@ -20,9 +21,10 @@ interface ExceptionsViewerHeaderProps { } /** - * Collection of filters and toggles for filtering exception items. + * Search exception items and take actions (to creat an item) */ const ExceptionsViewerHeaderComponent = ({ + isReadOnly, isInitLoading, listType, onFilterChange, @@ -81,17 +83,18 @@ const ExceptionsViewerHeaderComponent = ({ fullWidth /> - - - - {addExceptionButtonText} - - + {!isReadOnly && ( + + + {addExceptionButtonText} + + + )} ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx index 22c6e7dbf8ecf..aae467ca921a7 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx @@ -9,11 +9,11 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import * as i18n from '../translations'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; import { ExceptionsViewerItems } from './exceptions_viewer_items'; import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; import { TestProviders } from '../../../mock'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; const mockTheme = getMockTheme({ eui: { @@ -34,6 +34,9 @@ describe('ExceptionsViewerItems', () => { disableActions={false} exceptions={[]} loadingItemIds={[]} + listType={ExceptionListTypeEnum.DETECTION} + ruleReferences={null} + onCreateExceptionListItem={jest.fn()} onDeleteException={jest.fn()} onEditExceptionItem={jest.fn()} /> @@ -42,12 +45,6 @@ describe('ExceptionsViewerItems', () => { expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptTitle"]').last().text()).toEqual( - i18n.EXCEPTION_EMPTY_PROMPT_TITLE - ); - expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptBody"]').text()).toEqual( - i18n.EXCEPTION_EMPTY_PROMPT_BODY - ); }); it('it renders no search results found prompt if "showNoResults" is "true"', () => { @@ -61,6 +58,9 @@ describe('ExceptionsViewerItems', () => { disableActions={false} exceptions={[]} loadingItemIds={[]} + listType={ExceptionListTypeEnum.DETECTION} + ruleReferences={null} + onCreateExceptionListItem={jest.fn()} onDeleteException={jest.fn()} onEditExceptionItem={jest.fn()} /> @@ -68,12 +68,10 @@ describe('ExceptionsViewerItems', () => { ); - expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="exceptionItemsNoSearchResultsPrompt"]').exists() + ).toBeTruthy(); expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptTitle"]').last().text()).toEqual(''); - expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptBody"]').text()).toEqual( - i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY - ); }); it('it renders exceptions if "showEmpty" and "isInitLoading" is "false", and exceptions exist', () => { @@ -87,6 +85,9 @@ describe('ExceptionsViewerItems', () => { disableActions={false} exceptions={[getExceptionListItemSchemaMock()]} loadingItemIds={[]} + listType={ExceptionListTypeEnum.DETECTION} + ruleReferences={null} + onCreateExceptionListItem={jest.fn()} onDeleteException={jest.fn()} onEditExceptionItem={jest.fn()} /> @@ -105,18 +106,21 @@ describe('ExceptionsViewerItems', () => { ); expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="exceptionsLoadingPrompt"]').exists()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx index d6b9b743017af..84145b6bb117c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx @@ -18,6 +18,7 @@ import { ExceptionItemCard } from './exception_item_card'; import type { ExceptionListItemIdentifiers } from '../types'; import { ExeptionItemsViewerEmptySearchResults } from './no_search_results'; import { ExeptionItemsViewerNoItems } from './no_exception_items'; +import type { RuleReferences } from '../use_find_references'; const MyFlexItem = styled(EuiFlexItem)` margin: ${({ theme }) => `${theme.eui.euiSize} 0`}; @@ -34,7 +35,7 @@ interface ExceptionItemsViewerProps { exceptions: ExceptionListItemSchema[]; loadingItemIds: ExceptionListItemIdentifiers[]; listType: ExceptionListTypeEnum; - ruleReferences: unknown; + ruleReferences: RuleReferences | null; onCreateExceptionListItem: () => void; onDeleteException: (arg: ExceptionListItemIdentifiers) => void; onEditExceptionItem: (item: ExceptionListItemSchema) => void; @@ -81,9 +82,7 @@ const ExceptionItemsViewerComponent: React.FC = ({ loadingItemIds={loadingItemIds} exceptionItem={exception} listType={listType} - ruleReferences={ - ruleReferences != null ? ruleReferences[exception.list_id] : null - } + ruleReferences={ruleReferences != null ? ruleReferences[exception.list_id] : []} onDeleteException={onDeleteException} onEditException={onEditExceptionItem} dataTestSubj="exceptionItemsViewerItem" diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx index eeab8c7e36b70..e94b689fbfdc1 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { mount } from 'enzyme'; -import { ThemeProvider } from 'styled-components'; import { ExceptionsViewer } from '.'; import { useKibana } from '../../../lib/kibana'; @@ -16,20 +15,39 @@ import { useExceptionListItems, useApi } from '@kbn/securitysolution-list-hooks' import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock'; import { getFoundExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/found_exception_list_item_schema.mock'; -import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; - -const mockTheme = getMockTheme({ - eui: { - euiColorEmptyShade: '#ece', - euiBreakpoints: { - l: '1200px', - }, - euiSizeM: '10px', - }, -}); +import { TestProviders } from '../../../mock'; jest.mock('../../../lib/kibana'); jest.mock('@kbn/securitysolution-list-hooks'); +jest.mock('../use_find_references'); + +const mockRule = { + id: 'myfakeruleid', + author: [], + severity_mapping: [], + risk_score_mapping: [], + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', + references: [], + actions: [], + enabled: false, + false_positives: [], + max_signals: 100, + tags: [], + threat: [], + throttle: null, + version: 1, + exceptions_list: [], +}; describe('ExceptionsViewer', () => { const ruleName = 'test rule'; @@ -75,7 +93,7 @@ describe('ExceptionsViewer', () => { jest.fn(), ]); const wrapper = mount( - + { availableListTypes={[ExceptionListTypeEnum.DETECTION]} commentsAccordionId="commentsAccordion" /> - + ); expect(wrapper.find('[data-test-subj="loadingPanelAllRulesTable"]').exists()).toBeTruthy(); @@ -99,7 +117,7 @@ describe('ExceptionsViewer', () => { it('it renders empty prompt if no "exceptionListMeta" passed in', () => { const wrapper = mount( - + { availableListTypes={[ExceptionListTypeEnum.DETECTION]} commentsAccordionId="commentsAccordion" /> - + ); expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); @@ -128,7 +146,7 @@ describe('ExceptionsViewer', () => { ]); const wrapper = mount( - + { availableListTypes={[ExceptionListTypeEnum.DETECTION]} commentsAccordionId="commentsAccordion" /> - + ); expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index b47e19bd3953d..382e080c4e3fc 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -17,7 +17,7 @@ import type { import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { useApi, useExceptionListItems } from '@kbn/securitysolution-list-hooks'; -import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; +import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; import { useStateToaster } from '../../toasters'; import { useUserData } from '../../../../detections/components/user_info'; import { useKibana } from '../../../lib/kibana'; @@ -34,6 +34,7 @@ import { EditExceptionFlyout } from '../edit_exception_flyout'; import { AddExceptionFlyout } from '../add_exception_flyout'; import * as i18n from '../translations'; import { useFindExceptionListReferences } from '../use_find_references'; +import type { Rule } from '../../../../detections/containers/detection_engine/rules/types'; const initialState: State = { filterOptions: { filter: '', tags: [] }, @@ -54,20 +55,14 @@ const initialState: State = { }; interface ExceptionsViewerProps { - ruleId: string; - ruleName: string; - ruleIndices: string[]; - dataViewId?: string; + rule: Rule; exceptionListsMeta: ExceptionListIdentifiers[]; listType: ExceptionListTypeEnum; onRuleChange?: () => void; } const ExceptionsViewerComponent = ({ - ruleId, - ruleName, - ruleIndices, - dataViewId, + rule, exceptionListsMeta, listType, onRuleChange, @@ -107,12 +102,8 @@ const ExceptionsViewerComponent = ({ const { deleteExceptionItem, getExceptionListsItems } = useApi(services.http); const [isReadOnly, setReadOnly] = useState(true); const [lastUpdated, setLastUpdated] = useState(null); - - // With upcoming redesign for 8.5, you'll probably have all the lists already and won't - // need to fetch the rule - const { rule: maybeRule } = useRuleAsync(ruleId); const [isLoadingReferences, allReferences] = useFindExceptionListReferences( - maybeRule != null ? maybeRule?.exceptions_list ?? [] : [] + rule.exceptions_list ?? [] ); const [{ canUserCRUD, hasIndexWrite }] = useUserData(); @@ -344,17 +335,17 @@ const ExceptionsViewerComponent = ({ const showNoResults: boolean = exceptions.length === 0 && (totalEndpointItems > 0 || totalDetectionsItems > 0); - console.log({ allReferences }); + return ( <> {currentModal === 'editException' && exceptionToEdit != null && exceptionListTypeToEdit != null && ( - i18n.translate('xpack.securitySolution.exceptions.viewer.searchFilterLabel', { - values: { numberLists }, - defaultMessage: 'Exception {numberLists, plural, =1 {list} other {lists}} [{numberLists}]', - }); - -export const EXCEPTION_LISTS_FETCH_ERROR_TOASTER = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.exceptionListsFetchErrorDescription', - { - defaultMessage: 'Error fetching exception lists', - } -); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 9a24eb8579882..b5357cf03cf50 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -25,6 +25,7 @@ import type { import type { RulesSchema, GetInstalledIntegrationsResponse, + RulesReferencedByExceptionListsSchema, } from '../../../../../common/detection_engine/schemas/response'; import type { @@ -42,6 +43,7 @@ import type { BulkActionProps, BulkActionResponseMap, PreviewRulesProps, + FindRulesReferencedByExceptionsProps, } from './types'; import { KibanaServices } from '../../../../common/lib/kibana'; import * as i18n from '../../../pages/detection_engine/rules/translations'; @@ -373,9 +375,9 @@ export const fetchInstalledIntegrations = async ({ ); /** - * Fetch a Rule by providing a Rule ID + * Fetch info on what exceptions lists are referenced by what rules * - * @param id Rule ID's (not rule_id) + * @param lists exception list information needed for making request * @param http Kibana http service * @param signal to cancel request * @@ -385,15 +387,13 @@ export const findRuleExceptionReferences = async ({ lists, http, signal, -}: unknown & { http: HttpStart }): Promise => { - console.log({ LISTS: lists }) - return http.fetch(`${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, { +}: FindRulesReferencedByExceptionsProps): Promise => + http.fetch(`${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, { method: 'GET', query: { - list_ids: lists.map((l) => l.id).join(','), - list_list_ids: lists.map((l) => l.listId).join(','), - namespace_types: lists.map((l) => l.type).join(','), + ids: lists.map(({ id }) => id).join(','), + list_ids: lists.map(({ listId }) => listId).join(','), + namespace_types: lists.map(({ namespaceType }) => namespaceType).join(','), }, signal, }); -} \ No newline at end of file diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index eaf9b3288dc2d..dfe1a2feabcef 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -7,6 +7,8 @@ import * as t from 'io-ts'; +import type { HttpStart } from '@kbn/core/public'; +import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { listArray } from '@kbn/securitysolution-io-ts-list-types'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { @@ -341,3 +343,15 @@ export interface PrePackagedRulesStatusResponse { timelines_not_installed: number; timelines_not_updated: number; } + +export interface FindRulesReferencedByExceptionsListProp { + id: string; + listId: string; + namespaceType: NamespaceType; +} + +export interface FindRulesReferencedByExceptionsProps { + lists: FindRulesReferencedByExceptionsListProp[]; + http: HttpStart; + signal?: AbortSignal; +} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index 66892235a0f91..c4585d5e6e9ef 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -294,4 +294,66 @@ describe('RuleDetailsPageComponent', () => { }); }); }); + + it('renders exceptions tab', async () => { + await setup(); + (useRuleWithFallback as jest.Mock).mockReturnValue({ + error: null, + loading: false, + isExistingRule: true, + refresh: jest.fn(), + rule: { + ...mockRule, + outcome: 'conflict', + alias_target_id: 'aliased_rule_id', + alias_purpose: 'savedObjectConversion', + }, + }); + const wrapper = mount( + + + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="exceptionTab"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="endpointExceptionsTab"]').exists()).toBeFalsy(); + }); + }); + + it('renders endpoint exeptions tab when rule includes endpoint list', async () => { + await setup(); + (useRuleWithFallback as jest.Mock).mockReturnValue({ + error: null, + loading: false, + isExistingRule: true, + refresh: jest.fn(), + rule: { + ...mockRule, + outcome: 'conflict', + alias_target_id: 'aliased_rule_id', + alias_purpose: 'savedObjectConversion', + exceptions_list: [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + type: 'endpoint', + namespace_type: 'agnostic', + }, + ], + }, + }); + const wrapper = mount( + + + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="exceptionTab"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="endpointExceptionsTab"]').exists()).toBeTruthy(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index a1c9c04025877..593895d98e3cf 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -79,7 +79,7 @@ import { SecurityPageName } from '../../../../../app/types'; import { LinkButton } from '../../../../../common/components/links'; import { useFormatUrl } from '../../../../../common/components/link_to'; import { ExceptionsViewer } from '../../../../../common/components/exceptions/viewer'; -import { APP_UI_ID, DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants'; +import { APP_UI_ID } from '../../../../../../common/constants'; import { useGlobalFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; @@ -418,7 +418,7 @@ const RuleDetailsPageComponent: React.FC = ({ setTabs(visibleTabs); setRuleDetailTab(currentTab); - }, [hasIndexRead, ruleExecutionSettings]); + }, [hasIndexRead, ruleDetailTabs, ruleExecutionSettings]); const showUpdating = useMemo( () => isLoadingIndexPattern || isAlertsLoading || loading, @@ -880,26 +880,22 @@ const RuleDetailsPageComponent: React.FC = ({ )} )} - {ruleDetailTab === RuleDetailTabs.exceptions && ( + {rule != null && ruleDetailTab === RuleDetailTabs.exceptions && ( )} - {ruleDetailTab === RuleDetailTabs.endpointExceptions && ( + {rule != null && ruleDetailTab === RuleDetailTabs.endpointExceptions && ( )} {ruleDetailTab === RuleDetailTabs.executionResults && ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.test.ts new file mode 100644 index 0000000000000..de33a55524168 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.test.ts @@ -0,0 +1,154 @@ +/* + * 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 { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { + getEmptyFindResult, + getFindResultWithSingleHit, + getRuleMock, +} from '../__mocks__/request_responses'; +import { requestContextMock, serverMock, requestMock } from '../__mocks__'; +import { findRuleExceptionReferencesRoute } from './find_rule_exceptions_route'; +import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; + +describe('findRuleExceptionReferencesRoute', () => { + let server: ReturnType; + let { clients, context } = requestContextMock.createTools(); + + beforeEach(() => { + server = serverMock.create(); + ({ clients, context } = requestContextMock.createTools()); + + clients.rulesClient.find.mockResolvedValue({ + ...getFindResultWithSingleHit(), + data: [ + { + ...getRuleMock({ + ...getQueryRuleParams(), + exceptionsList: [ + { + type: 'detection', + id: '4656dc92-5832-11ea-8e2d-0242ac130003', + list_id: 'my_default_list', + namespace_type: 'single', + }, + ], + }), + }, + ], + }); + + findRuleExceptionReferencesRoute(server.router); + }); + + describe('happy paths', () => { + test('returns 200 when adding an exception item and rule_default exception list already exists', async () => { + const request = requestMock.create({ + method: 'get', + path: `${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, + query: { + ids: `4656dc92-5832-11ea-8e2d-0242ac130003`, + list_ids: `my_default_list`, + namespace_types: `single`, + }, + }); + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + references: [ + { + my_default_list: [ + { + exception_lists: [ + { + id: '4656dc92-5832-11ea-8e2d-0242ac130003', + list_id: 'my_default_list', + namespace_type: 'single', + type: 'detection', + }, + ], + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + name: 'Detect Root/Admin Users', + rule_id: 'rule-1', + }, + ], + }, + ], + }); + }); + + test('returns 200 when no references found', async () => { + const request = requestMock.create({ + method: 'get', + path: `${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, + query: { + ids: `4656dc92-5832-11ea-8e2d-0242ac130003`, + list_ids: `my_default_list`, + namespace_types: `single`, + }, + }); + + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); + + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + references: [ + { + my_default_list: [], + }, + ], + }); + }); + }); + + describe('error codes', () => { + test('returns 400 if query param lengths do not match', async () => { + const request = requestMock.create({ + method: 'get', + path: `${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, + query: { + ids: `4656dc92-5832-11ea-8e2d-0242ac130003`, + list_ids: `my_default_list`, + namespace_types: `single,agnostic`, + }, + }); + + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); + + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(400); + expect(response.body).toEqual({ + message: + '"ids", "list_ids" and "namespace_types" need to have the same comma separated number of values. Expected "ids" length: 1 to equal "namespace_types" length: 2 and "list_ids" legnth: 1.', + status_code: 400, + }); + }); + + test('returns 500 if rules client fails', async () => { + const request = requestMock.create({ + method: 'get', + path: `${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, + query: { + ids: `4656dc92-5832-11ea-8e2d-0242ac130003`, + list_ids: `my_default_list`, + namespace_types: `single`, + }, + }); + + clients.rulesClient.find.mockRejectedValue(new Error('find request failed')); + + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ message: 'find request failed', status_code: 500 }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts index ab082af8363df..17986310e774e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts @@ -6,28 +6,29 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; -import type { Logger } from '@kbn/core/server'; +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; +import type { FindResult } from '@kbn/alerting-plugin/server'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { buildSiemResponse } from '../utils'; import { enrichFilterWithRuleTypeMapping } from '../../rules/enrich_filter_with_rule_type_mappings'; -import type { FindExceptionReferencesOnRuleSchema } from '../../../../../common/detection_engine/schemas/request/find_exception_references_schema'; -import { findExceptionReferencesOnRuleSchema } from '../../../../../common/detection_engine/schemas/request/find_exception_references_schema'; +import type { FindExceptionReferencesOnRuleSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/find_exception_list_references_schema'; +import { findExceptionReferencesOnRuleSchema } from '../../../../../common/detection_engine/schemas/request/find_exception_list_references_schema'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -import { getSavedObjectType, getSavedObjectTypes } from '@kbn/securitysolution-list-utils'; +import type { RuleReferencesSchema } from '../../../../../common/detection_engine/schemas/response/find_exception_list_references_schema'; +import { rulesReferencedByExceptionListsSchema } from '../../../../../common/detection_engine/schemas/response/find_exception_list_references_schema'; +import type { RuleParams } from '../../schemas/rule_schemas'; -export const findRuleExceptionReferencesRoute = ( - router: SecuritySolutionPluginRouter, - logger: Logger -) => { +export const findRuleExceptionReferencesRoute = (router: SecuritySolutionPluginRouter) => { router.get( { path: `${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, validate: { query: buildRouteValidation< typeof findExceptionReferencesOnRuleSchema, - FindExceptionReferencesOnRuleSchema + FindExceptionReferencesOnRuleSchemaDecoded >(findExceptionReferencesOnRuleSchema), }, options: { @@ -38,51 +39,51 @@ export const findRuleExceptionReferencesRoute = ( const siemResponse = buildSiemResponse(response); try { - const { list_ids, namespace_types, list_list_ids } = request.query; + const { ids, namespace_types: namespaceTypes, list_ids: listIds } = request.query; + const ctx = await context.resolve(['core', 'securitySolution', 'alerting']); const rulesClient = ctx.alerting.getRulesClient(); - const results = await Promise.all( - list_ids.map(async (id, index) => { + if (ids.length !== namespaceTypes.length || ids.length !== listIds.length) { + return siemResponse.error({ + body: `"ids", "list_ids" and "namespace_types" need to have the same comma separated number of values. Expected "ids" length: ${ids.length} to equal "namespace_types" length: ${namespaceTypes.length} and "list_ids" legnth: ${listIds.length}.`, + statusCode: 400, + }); + } + + const results: Array> = await Promise.all( + ids.map(async (id, index) => { return rulesClient.find({ options: { perPage: 1000, filter: enrichFilterWithRuleTypeMapping(null), hasReference: { id, - type: getSavedObjectType({ namespaceType: namespace_types[index]}), + type: getSavedObjectType({ namespaceType: namespaceTypes[index] }), }, }, }); }) ); - const a = results.reduce((acc, data, index) => { - const wantedData = data.data.map(({ name, id, params}) => ({ + const references = results.reduce((acc, { data }, index) => { + const wantedData = data.map(({ name, id, params }) => ({ name, id, - ruleId: params.ruleId, - exceptionLists: params.exceptionsList, + rule_id: params.ruleId, + exception_lists: params.exceptionsList, })); - acc[list_list_ids[index]] = wantedData; - return acc; - }, {}); - console.log({ RESULT: JSON.stringify(a) }); - return response.ok({ body: a ?? {} }); - // const ruleIds = rules.data.map((rule) => rule.id); + return [...acc, { [listIds[index]]: wantedData }]; + }, []); - // const [ruleExecutionSummaries, ruleActions] = await Promise.all([ - // ruleExecutionLog.getExecutionSummariesBulk(ruleIds), - // legacyGetBulkRuleActionsSavedObject({ alertIds: ruleIds, savedObjectsClient, logger }), - // ]); + const [validated, errors] = validate({ references }, rulesReferencedByExceptionListsSchema); - // const transformed = transformFindAlerts(rules, ruleExecutionSummaries, ruleActions); - // if (transformed == null) { - // return siemResponse.error({ statusCode: 500, body: 'Internal error transforming' }); - // } else { - // return response.ok({ body: transformed ?? {} }); - // } + if (errors != null) { + return siemResponse.error({ statusCode: 500, body: errors }); + } else { + return response.ok({ body: validated ?? { references: [] } }); + } } catch (err) { const error = transformError(err); return siemResponse.error({ diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rule_exception_references.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rule_exception_references.ts new file mode 100644 index 0000000000000..e762a9e016486 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rule_exception_references.ts @@ -0,0 +1,184 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + CreateExceptionListSchema, + ExceptionListTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; +import { getCreateExceptionListMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createRule, + getSimpleRule, + createSignalsIndex, + deleteSignalsIndex, + deleteAllAlerts, + createExceptionList, +} from '../../utils'; +import { deleteAllExceptions } from '../../../lists_api_integration/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const log = getService('log'); + + describe.only('find_rule_exception_references', () => { + before(async () => { + await createSignalsIndex(supertest, log); + }); + + after(async () => { + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + }); + + afterEach(async () => { + await deleteAllExceptions(supertest, log); + }); + + it('returns empty array per list_id if no references are found', async () => { + // create exception list + const newExceptionList: CreateExceptionListSchema = { + ...getCreateExceptionListMinimalSchemaMock(), + list_id: 'i_exist', + namespace_type: 'single', + type: ExceptionListTypeEnum.DETECTION, + }; + const exceptionList = await createExceptionList(supertest, log, newExceptionList); + + // create rule + await createRule(supertest, log, getSimpleRule('rule-1')); + + const { body: references } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`) + .set('kbn-xsrf', 'true') + .query({ + ids: `${exceptionList.id}`, + list_ids: `${exceptionList.list_id}`, + namespace_types: `${exceptionList.namespace_type}`, + }) + .expect(200); + + expect(references).to.eql({ references: [{ i_exist: [] }] }); + }); + + it('returns empty array per list_id if list does not exist', async () => { + // create rule + await createRule(supertest, log, getSimpleRule('rule-1')); + + const { body: references } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`) + .set('kbn-xsrf', 'true') + .query({ + ids: `1234`, + list_ids: `i_dont_exist`, + namespace_types: `single`, + }) + .expect(200); + + expect(references).to.eql({ references: [{ i_dont_exist: [] }] }); + }); + + it('returns found references', async () => { + // create exception list + const newExceptionList: CreateExceptionListSchema = { + ...getCreateExceptionListMinimalSchemaMock(), + list_id: 'i_exist', + namespace_type: 'single', + type: ExceptionListTypeEnum.DETECTION, + }; + const exceptionList = await createExceptionList(supertest, log, newExceptionList); + const exceptionList2 = await createExceptionList(supertest, log, { + ...newExceptionList, + list_id: 'i_exist_2', + }); + + // create rule + const rule = await createRule(supertest, log, { + ...getSimpleRule('rule-2'), + exceptions_list: [ + { + id: `${exceptionList.id}`, + list_id: `${exceptionList.list_id}`, + namespace_type: `${exceptionList.namespace_type}`, + type: `${exceptionList.type}`, + }, + { + id: `${exceptionList2.id}`, + list_id: `${exceptionList2.list_id}`, + namespace_type: `${exceptionList2.namespace_type}`, + type: `${exceptionList2.type}`, + }, + ], + }); + + const { body: references } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`) + .set('kbn-xsrf', 'true') + .query({ + ids: `${exceptionList.id},${exceptionList2.id}`, + list_ids: `${exceptionList.list_id},${exceptionList2.list_id}`, + namespace_types: `${exceptionList.namespace_type},${exceptionList2.namespace_type}`, + }) + .expect(200); + + expect(references).to.eql({ + references: [ + { + i_exist: [ + { + exception_lists: [ + { + id: references.references[0].i_exist[0].exception_lists[0].id, + list_id: 'i_exist', + namespace_type: 'single', + type: 'detection', + }, + { + id: references.references[0].i_exist[0].exception_lists[1].id, + list_id: 'i_exist_2', + namespace_type: 'single', + type: 'detection', + }, + ], + id: rule.id, + name: 'Simple Rule Query', + rule_id: 'rule-2', + }, + ], + }, + { + i_exist_2: [ + { + exception_lists: [ + { + id: references.references[1].i_exist_2[0].exception_lists[0].id, + list_id: 'i_exist', + namespace_type: 'single', + type: 'detection', + }, + { + id: references.references[1].i_exist_2[0].exception_lists[1].id, + list_id: 'i_exist_2', + namespace_type: 'single', + type: 'detection', + }, + ], + id: rule.id, + name: 'Simple Rule Query', + rule_id: 'rule-2', + }, + ], + }, + ], + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts index 11b1e5e5deda0..9e6f71dbe1039 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts @@ -30,6 +30,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./delete_rules_bulk')); loadTestFile(require.resolve('./export_rules')); loadTestFile(require.resolve('./find_rules')); + loadTestFile(require.resolve('./find_rule_exception_references')); loadTestFile(require.resolve('./generating_signals')); loadTestFile(require.resolve('./get_prepackaged_rules_status')); loadTestFile(require.resolve('./get_rule_execution_results')); From c06e4977ab64c2f2f7e89ae7434dc9e9c9298698 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Sun, 14 Aug 2022 22:04:21 +0000 Subject: [PATCH 04/30] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../common/components/exceptions/viewer/exceptions_utility.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx index 7bc12b3a9b2f9..a4b8b29bc460a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx @@ -56,7 +56,7 @@ const ExceptionsViewerUtilityComponent: React.FC = - + Date: Mon, 15 Aug 2022 09:36:16 -0700 Subject: [PATCH 05/30] passing down wrong rule value --- .../public/common/components/exceptions/viewer/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index 382e080c4e3fc..273b47190b81a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -343,7 +343,7 @@ const ExceptionsViewerComponent = ({ exceptionListTypeToEdit != null && ( Date: Thu, 18 Aug 2022 10:54:14 -0700 Subject: [PATCH 06/30] addressing some lint/checks issues and starting cleanup --- .../src/common/index.ts | 1 + .../src/common/search/index.ts} | 11 +- .../find_exception_list_item_schema/index.ts | 2 + .../src/typescript_types/index.ts | 5 +- .../src/api/index.ts | 32 +- .../src/index.ts | 1 - .../src/use_api/index.ts | 4 +- .../src/use_exception_list_items/index.ts | 185 ------- .../routes/find_exception_list_item_route.ts | 2 + .../exception_lists/exception_list_client.ts | 4 + .../exception_list_client_types.ts | 3 + .../find_exception_list_items.ts | 4 + .../common/components/exceptions/types.ts | 12 - .../exceptions/viewer/empty_viewer_state.tsx | 100 ++++ .../viewer/exceptions_pagination.tsx | 118 +---- .../exceptions_viewer_header.stories.tsx | 69 --- .../viewer/exceptions_viewer_header.tsx | 87 ++-- .../viewer/exceptions_viewer_items.tsx | 68 +-- .../exceptions/viewer/index.test.tsx | 66 +-- .../components/exceptions/viewer/index.tsx | 487 +++++++++--------- .../components/exceptions/viewer/reducer.ts | 75 +-- .../exceptions/viewer/translations.ts | 37 +- .../use_initial_exceptions_viewer_render.tsx | 0 .../containers/detection_engine/rules/api.ts | 21 +- .../detection_engine/rules/details/index.tsx | 33 +- .../translations/translations/fr-FR.json | 22 - .../translations/translations/ja-JP.json | 22 - .../translations/translations/zh-CN.json | 22 - .../group1/find_rule_exception_references.ts | 2 +- 29 files changed, 560 insertions(+), 935 deletions(-) rename packages/{kbn-securitysolution-list-hooks/src/use_exception_list_items/index.test.ts => kbn-securitysolution-io-ts-list-types/src/common/search/index.ts} (53%) delete mode 100644 packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/empty_viewer_state.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/use_initial_exceptions_viewer_render.tsx diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/common/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/index.ts index fadf00fa7a40b..feadf8c775f5c 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/common/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/index.ts @@ -46,6 +46,7 @@ export * from './os_type'; export * from './page'; export * from './per_page'; export * from './pit'; +export * from './search'; export * from './search_after'; export * from './serializer'; export * from './sort_field'; diff --git a/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.ts similarity index 53% rename from packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.ts index 4ca0a66e4f602..c65c4dca5acab 100644 --- a/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.ts @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -describe('useExceptionListItems', () => { - test('Tests should be ported', () => { - // TODO: Port all the tests from: x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.test.ts here once mocks are figured out and kbn package mocks are figured out - expect(true).toBe(true); - }); -}); +import * as t from 'io-ts'; + +export const search = t.string; +export const searchFieldOrUndefined = t.union([search, t.undefined]); +export type SearchFieldOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.ts index 88756ac0eb301..0ca8140d048dc 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.ts @@ -21,6 +21,7 @@ import { import { RequiredKeepUndefined } from '../../common/required_keep_undefined'; import { sort_field } from '../../common/sort_field'; import { sort_order } from '../../common/sort_order'; +import { search } from '../../common/search'; export const findExceptionListItemSchema = t.intersection([ t.exact( @@ -34,6 +35,7 @@ export const findExceptionListItemSchema = t.intersection([ namespace_type: DefaultNamespaceArray, // defaults to ['single'] if not set during decode page: StringToPositiveNumber, // defaults to undefined if not set during decode per_page: StringToPositiveNumber, // defaults to undefined if not set during decode + search, sort_field, // defaults to undefined if not set during decode sort_order, // defaults to undefined if not set during decode }) diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts index a5eb4f976debd..a6d065298c731 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts @@ -99,7 +99,7 @@ export interface ExceptionListIdentifiers { export interface ApiCallFindListsItemsMemoProps { lists: ExceptionListIdentifiers[]; - filterOptions: FilterExceptionsOptions[]; + filters?: string; pagination: Partial; showDetectionsListsOnly: boolean; showEndpointListsOnly: boolean; @@ -168,8 +168,9 @@ export interface ApiCallByListIdProps { http: HttpStart; listIds: string[]; namespaceTypes: NamespaceType[]; - filterOptions: FilterExceptionsOptions[]; pagination: Partial; + search?: string; + filters?: string; signal: AbortSignal; } diff --git a/packages/kbn-securitysolution-list-api/src/api/index.ts b/packages/kbn-securitysolution-list-api/src/api/index.ts index a0361d044977c..8443edcbb13eb 100644 --- a/packages/kbn-securitysolution-list-api/src/api/index.ts +++ b/packages/kbn-securitysolution-list-api/src/api/index.ts @@ -34,8 +34,6 @@ import { import { ENDPOINT_LIST_URL, EXCEPTION_LIST_ITEM_URL, - EXCEPTION_LIST_NAMESPACE, - EXCEPTION_LIST_NAMESPACE_AGNOSTIC, EXCEPTION_LIST_URL, } from '@kbn/securitysolution-list-constants'; import { toError, toPromise } from '../fp_utils'; @@ -334,36 +332,20 @@ const fetchExceptionListsItemsByListIds = async ({ http, listIds, namespaceTypes, - filterOptions, + filters, pagination, + search, signal, }: ApiCallByListIdProps): Promise => { - const filters: string = filterOptions - .map((filter, index) => { - const namespace = namespaceTypes[index]; - const filterNamespace = - namespace === 'agnostic' ? EXCEPTION_LIST_NAMESPACE_AGNOSTIC : EXCEPTION_LIST_NAMESPACE; - const formattedFilters = [ - ...(filter.filter.length - ? [`${filterNamespace}.attributes.entries.field:${filter.filter}*`] - : []), - ...(filter.tags.length - ? filter.tags.map((t) => `${filterNamespace}.attributes.tags:${t}`) - : []), - ]; - - return formattedFilters.join(' AND '); - }) - .join(','); - const query = { list_id: listIds.join(','), namespace_type: namespaceTypes.join(','), page: pagination.page ? `${pagination.page}` : '1', per_page: pagination.perPage ? `${pagination.perPage}` : '20', + search, sort_field: 'exception-list.created_at', sort_order: 'desc', - ...(filters.trim() !== '' ? { filter: filters } : {}), + filter: filters, }; return http.fetch(`${EXCEPTION_LIST_ITEM_URL}/_find`, { @@ -374,11 +356,12 @@ const fetchExceptionListsItemsByListIds = async ({ }; const fetchExceptionListsItemsByListIdsWithValidation = async ({ - filterOptions, + filters, http, listIds, namespaceTypes, pagination, + search, signal, }: ApiCallByListIdProps): Promise => flow( @@ -386,11 +369,12 @@ const fetchExceptionListsItemsByListIdsWithValidation = async ({ tryCatch( () => fetchExceptionListsItemsByListIds({ - filterOptions, + filters, http, listIds, namespaceTypes, pagination, + search, signal, }), toError diff --git a/packages/kbn-securitysolution-list-hooks/src/index.ts b/packages/kbn-securitysolution-list-hooks/src/index.ts index 6e65c613de617..e458abd0448ad 100644 --- a/packages/kbn-securitysolution-list-hooks/src/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/index.ts @@ -10,7 +10,6 @@ export * from './use_api'; export * from './use_create_list_index'; export * from './use_cursor'; export * from './use_delete_list'; -export * from './use_exception_list_items'; export * from './use_exception_lists'; export * from './use_export_list'; export * from './use_find_lists'; diff --git a/packages/kbn-securitysolution-list-hooks/src/use_api/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_api/index.ts index 3b980f84d82a8..a651bb0f088e3 100644 --- a/packages/kbn-securitysolution-list-hooks/src/use_api/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/use_api/index.ts @@ -170,7 +170,7 @@ export const useApi = (http: HttpStart): ExceptionsApi => { }, async getExceptionListsItems({ lists, - filterOptions, + filters, pagination, showDetectionsListsOnly, showEndpointListsOnly, @@ -192,7 +192,7 @@ export const useApi = (http: HttpStart): ExceptionsApi => { per_page: perPage, total, } = await Api.fetchExceptionListsItemsByListIds({ - filterOptions, + filters, http, listIds: ids, namespaceTypes: namespaces, diff --git a/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.ts deleted file mode 100644 index 623e1e76a7f53..0000000000000 --- a/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* - * 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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { useEffect, useRef, useState } from 'react'; -import type { - ExceptionListItemSchema, - Pagination, - UseExceptionListProps, - FilterExceptionsOptions, -} from '@kbn/securitysolution-io-ts-list-types'; -import { fetchExceptionListsItemsByListIds } from '@kbn/securitysolution-list-api'; - -import { getIdsAndNamespaces } from '@kbn/securitysolution-list-utils'; -import { transformInput } from '../transforms'; - -type Func = () => void; -export type ReturnExceptionListAndItems = [ - boolean, - ExceptionListItemSchema[], - Pagination, - Func | null -]; - -/** - * Hook for using to get an ExceptionList and its ExceptionListItems - * - * @param http Kibana http service - * @param lists array of ExceptionListIdentifiers for all lists to fetch - * @param onError error callback - * @param onSuccess callback when all lists fetched successfully - * @param filterOptions optional - filter by fields or tags - * @param showDetectionsListsOnly boolean, if true, only detection lists are searched - * @param showEndpointListsOnly boolean, if true, only endpoint lists are searched - * @param matchFilters boolean, if true, applies first filter in filterOptions to - * all lists - * @param pagination optional - * - */ -export const useExceptionListItems = ({ - http, - lists, - pagination = { - page: 1, - perPage: 20, - total: 0, - }, - filterOptions, - showDetectionsListsOnly, - showEndpointListsOnly, - matchFilters, - onError, - onSuccess, -}: UseExceptionListProps): ReturnExceptionListAndItems => { - const [exceptionItems, setExceptionListItems] = useState([]); - const [paginationInfo, setPagination] = useState(pagination); - const fetchExceptionListsItems = useRef(null); - const [loading, setLoading] = useState(true); - const { ids, namespaces } = getIdsAndNamespaces({ - lists, - showDetection: showDetectionsListsOnly, - showEndpoint: showEndpointListsOnly, - }); - const filters: FilterExceptionsOptions[] = - matchFilters && filterOptions.length > 0 ? ids.map(() => filterOptions[0]) : filterOptions; - const idsAsString: string = ids.join(','); - const namespacesAsString: string = namespaces.join(','); - const filterAsString: string = filterOptions.map(({ filter }) => filter).join(','); - const filterTagsAsString: string = filterOptions.map(({ tags }) => tags.join(',')).join(','); - - useEffect( - () => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - - const fetchData = async (): Promise => { - try { - setLoading(true); - - if (ids.length === 0 && isSubscribed) { - setPagination({ - page: 0, - perPage: pagination.perPage, - total: 0, - }); - setExceptionListItems([]); - - if (onSuccess != null) { - onSuccess({ - exceptions: [], - pagination: { - page: 0, - perPage: pagination.perPage, - total: 0, - }, - }); - } - setLoading(false); - } else { - const { - page, - per_page: perPage, - total, - data, - } = await fetchExceptionListsItemsByListIds({ - filterOptions: filters, - http, - listIds: ids, - namespaceTypes: namespaces, - pagination: { - page: pagination.page, - perPage: pagination.perPage, - }, - signal: abortCtrl.signal, - }); - - // Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes - // for context around the temporary `id` - const transformedData = data.map((item) => transformInput(item)); - - if (isSubscribed) { - setPagination({ - page, - perPage, - total, - }); - setExceptionListItems(transformedData); - - if (onSuccess != null) { - onSuccess({ - exceptions: transformedData, - pagination: { - page, - perPage, - total, - }, - }); - } - } - } - } catch (error) { - if (isSubscribed) { - setExceptionListItems([]); - setPagination({ - page: 1, - perPage: 20, - total: 0, - }); - if (onError != null) { - onError(error); - } - } - } - - if (isSubscribed) { - setLoading(false); - } - }; - - fetchData(); - - fetchExceptionListsItems.current = fetchData; - return (): void => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, // eslint-disable-next-line react-hooks/exhaustive-deps - [ - http, - idsAsString, - namespacesAsString, - setExceptionListItems, - pagination.page, - pagination.perPage, - filterAsString, - filterTagsAsString, - ] - ); - - return [loading, exceptionItems, paginationInfo, fetchExceptionListsItems.current]; -}; diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index b28ebf26b012c..f77a3a7327d69 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -42,6 +42,7 @@ export const findExceptionListItemRoute = (router: ListsPluginRouter): void => { namespace_type: namespaceType, page, per_page: perPage, + search, sort_field: sortField, sort_order: sortOrder, } = request.query; @@ -59,6 +60,7 @@ export const findExceptionListItemRoute = (router: ListsPluginRouter): void => { page, perPage, pit: undefined, + search, searchAfter: undefined, sortField, sortOrder, diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index c586a9d764147..55f76f2135a80 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -764,6 +764,7 @@ export class ExceptionListClient { * @param options.perPage How many per page to return * @param options.pit The Point in Time (pit) id if there is one, otherwise "undefined" can be sent in * @param options.page The page number or "undefined" if there is no page number to continue from + * @param options.search The simple query search parameter if there is one, otherwise "undefined" can be sent in * @param options.searchAfter The search_after parameter if there is one, otherwise "undefined" can be sent in * @param options.sortField The sort field string if there is one, otherwise "undefined" can be sent in * @param options.sortOder The sort order string of "asc", "desc", otherwise "undefined" if there is no preference @@ -776,6 +777,7 @@ export class ExceptionListClient { perPage, pit, page, + search, searchAfter, sortField, sortOrder, @@ -793,6 +795,7 @@ export class ExceptionListClient { page, perPage, pit, + search, searchAfter, sortField, sortOrder, @@ -809,6 +812,7 @@ export class ExceptionListClient { perPage, pit, savedObjectsClient, + search, searchAfter, sortField, sortOrder, diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index 2b3a800ac5a5a..336c7f38208ff 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -46,6 +46,7 @@ import type { PitId, PitOrUndefined, SearchAfterOrUndefined, + SearchOrUndefined, SortFieldOrUndefined, SortOrderOrUndefined, Tags, @@ -407,6 +408,8 @@ export interface FindExceptionListsItemOptions { perPage: PerPageOrUndefined; /** The Point in Time (pit) id if there is one, otherwise "undefined" can be sent in */ pit?: PitOrUndefined; + /** The simple search parameter if there is one, otherwise "undefined" can be sent in */ + search?: SearchOrUndefined; /** The search_after parameter if there is one, otherwise "undefined" can be sent in */ searchAfter?: SearchAfterOrUndefined; /** The page number or "undefined" if there is no page number to continue from */ diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts index f0e1fa07749f8..7a58078dc4e6e 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts @@ -13,6 +13,7 @@ import type { PerPageOrUndefined, PitOrUndefined, SearchAfterOrUndefined, + SearchOrUndefined, SortFieldOrUndefined, SortOrderOrUndefined, } from '@kbn/securitysolution-io-ts-list-types'; @@ -39,6 +40,7 @@ interface FindExceptionListItemsOptions { sortField: SortFieldOrUndefined; sortOrder: SortOrderOrUndefined; searchAfter: SearchAfterOrUndefined; + search?: SearchOrUndefined; } export const findExceptionListsItem = async ({ @@ -49,6 +51,7 @@ export const findExceptionListsItem = async ({ page, pit, perPage, + search, searchAfter, sortField, sortOrder, @@ -74,6 +77,7 @@ export const findExceptionListsItem = async ({ page, perPage, pit, + search, searchAfter, sortField, sortOrder, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts index fe0f137800d26..89b7783f58ec2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts @@ -14,18 +14,6 @@ export interface ExceptionListItemIdentifiers { namespaceType: NamespaceType; } -export interface FilterOptions { - filter: string; - tags: string[]; -} - -export interface Filter { - filter: Partial; - pagination: Partial; - showDetectionsListsOnly: boolean; - showEndpointListsOnly: boolean; -} - export interface ExceptionsPagination { pageIndex: number; pageSize: number; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/empty_viewer_state.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/empty_viewer_state.tsx new file mode 100644 index 0000000000000..5af7998135144 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/empty_viewer_state.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiEmptyPrompt, EuiEmptyPromptProps } from '@elastic/eui'; +import { EuiButton, useEuiTheme, EuiPageTemplate, EuiLoadingLogo } from '@elastic/eui'; + +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import * as i18n from './translations'; + +interface ExeptionItemsViewerNoItemsComponentProps { + listType: ExceptionListTypeEnum; + currentState: string; + onCreateExceptionListItem: () => void; +} + +const ExeptionItemsViewerEmptyPromptsComponent = ({ + listType, + currentState, + onCreateExceptionListItem, +}: ExeptionItemsViewerNoItemsComponentProps): JSX.Element => { + const { euiTheme } = useEuiTheme(); + + let emptyPromptProps: Partial; + switch (currentState) { + case 'error': + emptyPromptProps = { + color: 'danger', + iconType: 'alert', + title:

{i18n.EXCEPTION_ERROR_TITLE}

, + body:

{i18n.EXCEPTION_ERROR_DESCRIPTION}

, + }; + break; + case 'empty': + emptyPromptProps = { + color: 'subdued', + iconType: 'plusInCircle', + iconColor: euiTheme.colors.darkestShade, + title: ( +

{i18n.EXCEPTION_EMPTY_PROMPT_TITLE}

+ ), + body: ( +

+ {listType === ExceptionListTypeEnum.ENDPOINT + ? i18n.EXCEPTION_EMPTY_ENDPOINT_PROMPT_BODY + : i18n.EXCEPTION_EMPTY_PROMPT_BODY} +

+ ), + actions: [ + + {listType === ExceptionListTypeEnum.ENDPOINT + ? i18n.EXCEPTION_EMPTY_PROMPT_ENDPOINT_BUTTON + : i18n.EXCEPTION_EMPTY_PROMPT_BUTTON} + , + ], + }; + break; + case 'empty_search': + emptyPromptProps = { + color: 'subdued', + title: ( +

+ {i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_TITLE} +

+ ), + body: ( +

+ {i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY} +

+ ), + }; + break; + default: + emptyPromptProps = { + color: 'subdued', + icon: , + title:

{i18n.EXCEPTION_LOADING_TITLE}

, + }; + break; + } + + return ( + + + + ); +}; + +export const ExeptionItemsViewerEmptyPrompts = React.memo(ExeptionItemsViewerEmptyPromptsComponent); + +ExeptionItemsViewerEmptyPrompts.displayName = 'ExeptionItemsViewerEmptyPrompts'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx index c64130e7eb56d..f53b7c4536150 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx @@ -5,116 +5,50 @@ * 2.0. */ -import type { ReactElement } from 'react'; -import React, { useCallback, useState, useMemo } from 'react'; -import { - EuiContextMenuItem, - EuiButtonEmpty, - EuiPagination, - EuiFlexItem, - EuiFlexGroup, - EuiPopover, - EuiContextMenuPanel, -} from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { EuiTablePagination } from '@elastic/eui'; -import * as i18n from '../translations'; -import type { ExceptionsPagination, Filter } from '../types'; +import type { ExceptionsPagination } from '../types'; interface ExceptionsViewerPaginationProps { pagination: ExceptionsPagination; - onPaginationChange: (arg: Partial) => void; + onPaginationChange: (arg: { page: number; perPage: number }) => void; } const ExceptionsViewerPaginationComponent = ({ pagination, onPaginationChange, }: ExceptionsViewerPaginationProps): JSX.Element => { - const [isOpen, setIsOpen] = useState(false); - - const handleClosePerPageMenu = useCallback((): void => setIsOpen(false), [setIsOpen]); - - const handlePerPageMenuClick = useCallback( - (): void => setIsOpen((isPopoverOpen) => !isPopoverOpen), - [setIsOpen] + const handleItemsPerPageChange = useCallback( + (pageSize: number) => { + onPaginationChange({ + page: pagination.pageIndex, + perPage: pageSize, + }); + }, + [onPaginationChange, pagination.pageIndex] ); - const handlePageClick = useCallback( - (pageIndex: number): void => { + const handlePageIndexChange = useCallback( + (pageIndex: number) => { onPaginationChange({ - pagination: { - pageIndex, - pageSize: pagination.pageSize, - totalItemCount: pagination.totalItemCount, - }, + page: pageIndex, + perPage: pagination.pageSize, }); }, - [pagination, onPaginationChange] + [onPaginationChange, pagination.pageSize] ); - const items = useMemo((): ReactElement[] => { - return pagination.pageSizeOptions.map((rows) => ( - { - onPaginationChange({ - pagination: { - pageIndex: 0, - pageSize: rows, - totalItemCount: pagination.totalItemCount, - }, - }); - handleClosePerPageMenu(); - }} - data-test-subj="exceptionsPerPageItem" - > - {i18n.NUMBER_OF_ITEMS(rows)} - - )); - }, [pagination, onPaginationChange, handleClosePerPageMenu]); - - const totalPages = useMemo((): number => { - if (pagination.totalItemCount > 0) { - return Math.ceil(pagination.totalItemCount / pagination.pageSize); - } else { - return 1; - } - }, [pagination]); - return ( - - - - {i18n.ITEMS_PER_PAGE(pagination.pageSize)} - - } - isOpen={isOpen} - closePopover={handleClosePerPageMenu} - panelPaddingSize="none" - repositionOnScroll - > - - - - - - - - + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx deleted file mode 100644 index 05cbe352fa72e..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 { storiesOf, addDecorator } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { euiLightVars } from '@kbn/ui-theme'; - -import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { ExceptionsViewerHeader } from './exceptions_viewer_header'; - -addDecorator((storyFn) => ( - ({ eui: euiLightVars, darkMode: false })}>{storyFn()} -)); - -storiesOf('Components/ExceptionsViewerHeader', module) - .add('loading', () => { - return ( - - ); - }) - .add('all lists', () => { - return ( - - ); - }) - .add('endpoint only', () => { - return ( - - ); - }) - .add('detections only', () => { - return ( - - ); - }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx index 7d1a955d372cd..b51ff0f4e81f9 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx @@ -5,18 +5,46 @@ * 2.0. */ -import React, { useEffect, useState, useCallback, useMemo } from 'react'; -import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiSearchBar } from '@elastic/eui'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import * as i18n from '../translations'; -import type { Filter } from '../types'; + +const ITEMS_SCHEMA = { + strict: true, + fields: { + created_by: { + type: 'string', + }, + description: { + type: 'string', + }, + id: { + type: 'string', + }, + item_id: { + type: 'string', + }, + list_id: { + type: 'string', + }, + name: { + type: 'string', + }, + os_types: { + type: 'string', + }, + tags: { + type: 'string', + }, + }, +}; interface ExceptionsViewerHeaderProps { isReadOnly: boolean; - isInitLoading: boolean; listType: ExceptionListTypeEnum; - onFilterChange: (arg: Partial) => void; + onSearch: (arg: string) => void; onAddExceptionClick: (type: ExceptionListTypeEnum) => void; } @@ -25,39 +53,15 @@ interface ExceptionsViewerHeaderProps { */ const ExceptionsViewerHeaderComponent = ({ isReadOnly, - isInitLoading, listType, - onFilterChange, + onSearch, onAddExceptionClick, }: ExceptionsViewerHeaderProps): JSX.Element => { - const [filter, setFilter] = useState(''); - const [tags, setTags] = useState([]); - - useEffect((): void => { - onFilterChange({ - filter: { filter, tags }, - pagination: { - pageIndex: 0, - }, - showDetectionsListsOnly: listType !== ExceptionListTypeEnum.ENDPOINT, - showEndpointListsOnly: listType === ExceptionListTypeEnum.ENDPOINT, - }); - }, [filter, tags, onFilterChange, listType]); - const handleOnSearch = useCallback( - (searchValue: string): void => { - const tagsRegex = /(tags:[^\s]*)/i; - const tagsMatch = searchValue.match(tagsRegex); - const foundTags: string = tagsMatch != null ? tagsMatch[0].split(':')[1] : ''; - const filterString = tagsMatch != null ? searchValue.replace(tagsRegex, '') : searchValue; - - if (foundTags.length > 0) { - setTags(foundTags.split(',')); - } - - setFilter(filterString.trim()); + ({ queryText }): void => { + onSearch(queryText); }, - [setTags, setFilter] + [onSearch] ); const handleAddException = useCallback(() => { @@ -73,14 +77,14 @@ const ExceptionsViewerHeaderComponent = ({ return ( - {!isReadOnly && ( @@ -88,7 +92,6 @@ const ExceptionsViewerHeaderComponent = ({ {addExceptionButtonText} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx index 84145b6bb117c..41cccc1cf8ba8 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx @@ -6,18 +6,16 @@ */ import React from 'react'; -import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiLoadingLogo } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import type { ExceptionListItemSchema, ExceptionListTypeEnum, } from '@kbn/securitysolution-io-ts-list-types'; -import * as i18n from './translations'; + import { ExceptionItemCard } from './exception_item_card'; import type { ExceptionListItemIdentifiers } from '../types'; -import { ExeptionItemsViewerEmptySearchResults } from './no_search_results'; -import { ExeptionItemsViewerNoItems } from './no_exception_items'; import type { RuleReferences } from '../use_find_references'; const MyFlexItem = styled(EuiFlexItem)` @@ -28,23 +26,16 @@ const MyFlexItem = styled(EuiFlexItem)` `; interface ExceptionItemsViewerProps { - showEmpty: boolean; - showNoResults: boolean; - isInitLoading: boolean; disableActions: boolean; exceptions: ExceptionListItemSchema[]; loadingItemIds: ExceptionListItemIdentifiers[]; listType: ExceptionListTypeEnum; ruleReferences: RuleReferences | null; - onCreateExceptionListItem: () => void; onDeleteException: (arg: ExceptionListItemIdentifiers) => void; onEditExceptionItem: (item: ExceptionListItemSchema) => void; } const ExceptionItemsViewerComponent: React.FC = ({ - showEmpty, - showNoResults, - isInitLoading, exceptions, loadingItemIds, listType, @@ -52,46 +43,27 @@ const ExceptionItemsViewerComponent: React.FC = ({ ruleReferences, onDeleteException, onEditExceptionItem, - onCreateExceptionListItem, }): JSX.Element => { return ( - {isInitLoading && ( - } - title={

{i18n.EXCEPTION_LOADING_TITLE}

} - data-test-subj="exceptionsLoadingPrompt" - /> - )} - {showNoResults && } - {showEmpty && ( - - )} - {!showEmpty && !showNoResults && !isInitLoading && ( - - - {!isInitLoading && - exceptions.length > 0 && - exceptions.map((exception) => ( - - - - ))} - - - )} + + + {exceptions.map((exception) => ( + + + + ))} + +
); }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx index e94b689fbfdc1..2d21c5fef200c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx @@ -16,42 +16,27 @@ import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock'; import { getFoundExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/found_exception_list_item_schema.mock'; import { TestProviders } from '../../../mock'; +import type { Rule } from '../../../../detections/containers/detection_engine/rules/types'; +import { mockRule } from '../../../../detections/pages/detection_engine/rules/all/__mocks__/mock'; +import { useFindExceptionListReferences } from '../use_find_references'; jest.mock('../../../lib/kibana'); jest.mock('@kbn/securitysolution-list-hooks'); jest.mock('../use_find_references'); -const mockRule = { - id: 'myfakeruleid', - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - query: 'some query', - index: ['index-1'], - interval: '5m', - references: [], - actions: [], - enabled: false, - false_positives: [], - max_signals: 100, - tags: [], - threat: [], - throttle: null, - version: 1, - exceptions_list: [], -}; +const getMockRule = (): Rule => ({ + ...mockRule('123'), + exceptions_list: [ + { + id: '5b543420', + list_id: 'list_id', + type: 'endpoint', + namespace_type: 'single', + }, + ], +}); describe('ExceptionsViewer', () => { - const ruleName = 'test rule'; - beforeEach(() => { (useKibana as jest.Mock).mockReturnValue({ services: { @@ -78,6 +63,8 @@ describe('ExceptionsViewer', () => { }, jest.fn(), ]); + + (useFindExceptionListReferences as jest.Mock).mockReturnValue([false, null]); }); it('it renders loader if "loadingList" is true', () => { @@ -95,9 +82,7 @@ describe('ExceptionsViewer', () => { const wrapper = mount( { namespaceType: 'single', }, ]} - availableListTypes={[ExceptionListTypeEnum.DETECTION]} - commentsAccordionId="commentsAccordion" + listType={ExceptionListTypeEnum.DETECTION} /> ); @@ -119,12 +103,9 @@ describe('ExceptionsViewer', () => { const wrapper = mount( ); @@ -148,9 +129,7 @@ describe('ExceptionsViewer', () => { const wrapper = mount( { namespaceType: 'single', }, ]} - availableListTypes={[ExceptionListTypeEnum.DETECTION]} - commentsAccordionId="commentsAccordion" + listType={ExceptionListTypeEnum.DETECTION} /> ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index 273b47190b81a..2c3525ed74472 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -7,24 +7,25 @@ import React, { useCallback, useEffect, useReducer, useState } from 'react'; import { EuiPanel, EuiSpacer } from '@elastic/eui'; -import uuid from 'uuid'; import type { ExceptionListItemSchema, - ExceptionListIdentifiers, UseExceptionListItemsSuccess, + Pagination, + ExceptionListTypeEnum, } from '@kbn/securitysolution-io-ts-list-types'; -import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { useApi, useExceptionListItems } from '@kbn/securitysolution-list-hooks'; +import { transformInput } from '@kbn/securitysolution-list-hooks'; +import { + deleteExceptionListItemById, + fetchExceptionListsItemsByListIds, +} from '@kbn/securitysolution-list-api'; import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; -import { useStateToaster } from '../../toasters'; import { useUserData } from '../../../../detections/components/user_info'; -import { useKibana } from '../../../lib/kibana'; -import { Loader } from '../../loader'; +import { useKibana, useToasts } from '../../../lib/kibana'; import { ExceptionsViewerHeader } from './exceptions_viewer_header'; -import type { ExceptionListItemIdentifiers, Filter } from '../types'; -import type { State, ViewerFlyoutName } from './reducer'; +import type { ExceptionListItemIdentifiers } from '../types'; +import type { State, ViewerFlyoutName, ViewerState } from './reducer'; import { allExceptionItemsReducer } from './reducer'; import { ExceptionsViewerPagination } from './exceptions_pagination'; @@ -32,309 +33,302 @@ import { ExceptionsViewerUtility } from './exceptions_utility'; import { ExceptionsViewerItems } from './exceptions_viewer_items'; import { EditExceptionFlyout } from '../edit_exception_flyout'; import { AddExceptionFlyout } from '../add_exception_flyout'; -import * as i18n from '../translations'; +import * as i18n from './translations'; import { useFindExceptionListReferences } from '../use_find_references'; import type { Rule } from '../../../../detections/containers/detection_engine/rules/types'; +import { ExeptionItemsViewerEmptyPrompts } from './empty_viewer_state'; const initialState: State = { - filterOptions: { filter: '', tags: [] }, pagination: { pageIndex: 0, - pageSize: 20, + pageSize: 25, totalItemCount: 0, - pageSizeOptions: [5, 10, 20, 50, 100, 200, 300], + pageSizeOptions: [1, 5, 10, 25, 50, 100, 200, 300], }, exceptions: [], exceptionToEdit: null, loadingItemIds: [], - isInitLoading: true, currentModal: null, exceptionListTypeToEdit: null, - totalEndpointItems: 0, - totalDetectionsItems: 0, + viewerState: 'loading', + exceptionLists: [], }; interface ExceptionsViewerProps { rule: Rule; - exceptionListsMeta: ExceptionListIdentifiers[]; listType: ExceptionListTypeEnum; onRuleChange?: () => void; } const ExceptionsViewerComponent = ({ rule, - exceptionListsMeta, listType, onRuleChange, }: ExceptionsViewerProps): JSX.Element => { const { services } = useKibana(); - const [, dispatchToaster] = useStateToaster(); - const onDispatchToaster = useCallback( - ({ title, color, iconType }) => - (): void => { - dispatchToaster({ - type: 'addToaster', - toast: { - id: uuid.v4(), - title, - color, - iconType, - }, - }); - }, - [dispatchToaster] - ); + const toasts = useToasts(); + const [{ canUserCRUD, hasIndexWrite }] = useUserData(); + const [isReadOnly, setReadOnly] = useState(true); + const [lastUpdated, setLastUpdated] = useState(null); + + // Reducer state const [ { exceptions, - filterOptions, pagination, loadingItemIds, - isInitLoading, currentModal, exceptionToEdit, exceptionListTypeToEdit, - totalEndpointItems, - totalDetectionsItems, + viewerState, + exceptionLists, }, dispatch, - ] = useReducer(allExceptionItemsReducer(), { ...initialState }); - const { deleteExceptionItem, getExceptionListsItems } = useApi(services.http); - const [isReadOnly, setReadOnly] = useState(true); - const [lastUpdated, setLastUpdated] = useState(null); - const [isLoadingReferences, allReferences] = useFindExceptionListReferences( - rule.exceptions_list ?? [] - ); - - const [{ canUserCRUD, hasIndexWrite }] = useUserData(); - - useEffect((): void => { - if (!canUserCRUD || !hasIndexWrite) { - setReadOnly(true); - } else { - setReadOnly(false); - } - }, [setReadOnly, canUserCRUD, hasIndexWrite]); + ] = useReducer(allExceptionItemsReducer(), { + ...initialState, + exceptionLists: + rule != null && rule.exceptions_list != null + ? rule.exceptions_list.filter((list) => list.type === listType) + : [], + }); + // Reducer actions const setExceptions = useCallback( ({ exceptions: newExceptions, pagination: newPagination, }: UseExceptionListItemsSuccess): void => { + setLastUpdated(Date.now()); + dispatch({ type: 'setExceptions', - lists: exceptionListsMeta, exceptions: newExceptions, pagination: newPagination, }); }, - [dispatch, exceptionListsMeta] + [dispatch] ); - const [loadingList, , , fetchListItems] = useExceptionListItems({ - http: services.http, - lists: exceptionListsMeta, - filterOptions: - filterOptions.filter !== '' || filterOptions.tags.length > 0 ? [filterOptions] : [], - pagination: { - page: pagination.pageIndex + 1, - perPage: pagination.pageSize, - total: pagination.totalItemCount, - }, - showDetectionsListsOnly: listType !== ExceptionListTypeEnum.ENDPOINT, - showEndpointListsOnly: listType === ExceptionListTypeEnum.ENDPOINT, - matchFilters: true, - onSuccess: setExceptions, - onError: onDispatchToaster({ - color: 'danger', - title: i18n.FETCH_LIST_ERROR, - iconType: 'alert', - }), - }); - const setCurrentModal = useCallback( - (modalName: ViewerFlyoutName): void => { + const setViewerState = useCallback( + (state: ViewerState): void => { dispatch({ - type: 'updateModalOpen', - modalName, + type: 'setViewerState', + state, }); }, [dispatch] ); - const setExceptionItemTotals = useCallback( - (endpointItemTotals: number | null, detectionItemTotals: number | null): void => { + const setFlyoutType = useCallback( + (flyoutType: ViewerFlyoutName): void => { dispatch({ - type: 'setExceptionItemTotals', - totalEndpointItems: endpointItemTotals, - totalDetectionsItems: detectionItemTotals, + type: 'updateFlyoutOpen', + flyoutType, }); }, [dispatch] ); - const handleGetTotals = useCallback(async (): Promise => { - if (listType !== ExceptionListTypeEnum.ENDPOINT) { - await getExceptionListsItems({ - lists: exceptionListsMeta, - filterOptions: [], - pagination: { - page: 0, - perPage: 1, - total: 0, - }, - showDetectionsListsOnly: true, - showEndpointListsOnly: false, - onSuccess: ({ pagination: detectionPagination }) => { - setExceptionItemTotals(null, detectionPagination.total ?? 0); - }, - onError: () => { - const dispatchToasterError = onDispatchToaster({ - color: 'danger', - title: i18n.TOTAL_ITEMS_FETCH_ERROR, - iconType: 'alert', - }); - - dispatchToasterError(); - }, - }); - } else if (listType === ExceptionListTypeEnum.ENDPOINT) { - await getExceptionListsItems({ - lists: exceptionListsMeta, - filterOptions: [], - pagination: { - page: 0, - perPage: 1, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: true, - onSuccess: ({ pagination: endpointPagination }) => { - setExceptionItemTotals(endpointPagination.total ?? 0, null); - }, - onError: () => { - const dispatchToasterError = onDispatchToaster({ - color: 'danger', - title: i18n.TOTAL_ITEMS_FETCH_ERROR, - iconType: 'alert', - }); - - dispatchToasterError(); - }, - }); - } - }, [ - listType, - getExceptionListsItems, - exceptionListsMeta, - setExceptionItemTotals, - onDispatchToaster, - ]); - - const handleFetchList = useCallback((): void => { - if (fetchListItems != null) { - fetchListItems(); - handleGetTotals(); - } - }, [fetchListItems, handleGetTotals]); - - const handleFilterChange = useCallback( - (filters: Partial): void => { + const setLoadingItemIds = useCallback( + (items: ExceptionListItemIdentifiers[]): void => { dispatch({ - type: 'updateFilterOptions', - filters, + type: 'updateLoadingItemIds', + items, }); }, [dispatch] ); + const [_, allReferences] = useFindExceptionListReferences( + rule != null ? rule.exceptions_list ?? [] : [] + ); + + const handleFetchItems = useCallback( + async (options?: { pagination?: Partial; search?: string; filters?: string }) => { + const abortCtrl = new AbortController(); + + const newPagination = + options?.pagination != null + ? { + page: options.pagination.page + 1, + perPage: options.pagination.perPage, + } + : { + page: pagination.pageIndex + 1, + perPage: pagination.pageSize, + }; + + const { + page: pageIndex, + per_page: itemsPerPage, + total, + data, + } = await fetchExceptionListsItemsByListIds({ + filters: undefined, + http: services.http, + listIds: exceptionLists.map((list) => list.list_id), + namespaceTypes: exceptionLists.map((list) => list.namespace_type), + search: options?.search, + pagination: newPagination, + signal: abortCtrl.signal, + }); + + // Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes + // for context around the temporary `id` + const transformedData = data.map((item) => transformInput(item)); + + return { + data: transformedData, + pageIndex, + itemsPerPage, + total, + }; + }, + [pagination.pageIndex, pagination.pageSize, exceptionLists, services.http] + ); + + const handleGetExceptionListItems = useCallback( + async (options?: { page: number; perPage: number }) => { + try { + setViewerState('loading'); + + const { pageIndex, itemsPerPage, total, data } = await handleFetchItems({ + pagination: { + page: options?.page ?? pagination.pageIndex, + perPage: options?.perPage ?? pagination.pageSize, + }, + }); + + setViewerState(total > 0 ? null : 'empty'); + + setExceptions({ + exceptions: data, + pagination: { + page: pageIndex, + perPage: itemsPerPage, + total, + }, + }); + } catch (e) { + setViewerState('error'); + + toasts.addError(e, { + title: i18n.EXCEPTION_ERROR_TITLE, + toastMessage: i18n.EXCEPTION_ERROR_DESCRIPTION, + }); + } + }, + [ + handleFetchItems, + setExceptions, + setViewerState, + pagination.pageSize, + pagination.pageIndex, + toasts, + ] + ); + + const handleSearch = useCallback( + async (search: string) => { + try { + setViewerState('searching'); + + const { pageIndex, itemsPerPage, total, data } = await handleFetchItems({ search }); + + setViewerState(total > 0 ? null : 'empty_search'); + + setExceptions({ + exceptions: data, + pagination: { + page: pageIndex, + perPage: itemsPerPage, + total, + }, + }); + } catch (e) { + toasts.addError(e, { + title: i18n.EXCEPTION_SEARCH_ERROR_TITLE, + toastMessage: i18n.EXCEPTION_SEARCH_ERROR_BODY, + }); + } + }, + [handleFetchItems, setExceptions, setViewerState, toasts] + ); + const handleAddException = useCallback((): void => { dispatch({ type: 'updateExceptionListTypeToEdit', exceptionListType: listType, }); - setCurrentModal('addException'); - }, [listType, setCurrentModal]); + setFlyoutType('addException'); + }, [listType, setFlyoutType]); const handleEditException = useCallback( (exception: ExceptionListItemSchema): void => { dispatch({ type: 'updateExceptionToEdit', - lists: exceptionListsMeta, + lists: exceptionLists, exception, }); - setCurrentModal('editException'); + setFlyoutType('editException'); }, - [setCurrentModal, exceptionListsMeta] + [setFlyoutType, exceptionLists] ); - const handleOnCancelExceptionModal = useCallback((): void => { - setCurrentModal(null); - handleFetchList(); - }, [setCurrentModal, handleFetchList]); - - const handleOnConfirmExceptionModal = useCallback((): void => { - setCurrentModal(null); - handleFetchList(); - }, [setCurrentModal, handleFetchList]); + const handleCancelExceptionItemFlyout = useCallback((): void => { + setFlyoutType(null); + handleGetExceptionListItems(); + }, [setFlyoutType, handleGetExceptionListItems]); - const setLoadingItemIds = useCallback( - (items: ExceptionListItemIdentifiers[]): void => { - dispatch({ - type: 'updateLoadingItemIds', - items, - }); - }, - [dispatch] - ); + const handleConfirmExceptionFlyout = useCallback((): void => { + setFlyoutType(null); + handleGetExceptionListItems(); + }, [setFlyoutType, handleGetExceptionListItems]); const handleDeleteException = useCallback( - ({ id: itemId, namespaceType }: ExceptionListItemIdentifiers) => { - setLoadingItemIds([{ id: itemId, namespaceType }]); - - deleteExceptionItem({ - id: itemId, - namespaceType, - onSuccess: () => { - setLoadingItemIds(loadingItemIds.filter(({ id }) => id !== itemId)); - handleFetchList(); - }, - onError: () => { - const dispatchToasterError = onDispatchToaster({ - color: 'danger', - title: i18n.DELETE_EXCEPTION_ERROR, - iconType: 'alert', - }); - - dispatchToasterError(); - setLoadingItemIds(loadingItemIds.filter(({ id }) => id !== itemId)); - }, - }); + async ({ id: itemId, namespaceType }: ExceptionListItemIdentifiers) => { + const abortCtrl = new AbortController(); + + try { + setLoadingItemIds([{ id: itemId, namespaceType }]); + + await deleteExceptionListItemById({ + http: services.http, + id: itemId, + namespaceType, + signal: abortCtrl.signal, + }); + + setLoadingItemIds(loadingItemIds.filter(({ id }) => id !== itemId)); + } catch (e) { + setLoadingItemIds(loadingItemIds.filter(({ id }) => id !== itemId)); + + setViewerState('error'); + + toasts.addError(e, { + title: i18n.EXCEPTION_DELETE_ERROR_TITLE, + }); + } }, - [setLoadingItemIds, deleteExceptionItem, loadingItemIds, handleFetchList, onDispatchToaster] + [setLoadingItemIds, services.http, loadingItemIds, setViewerState, toasts] ); - // Logic for initial render + // User privileges checks useEffect((): void => { - if ( - isInitLoading && - !isLoadingReferences && - !loadingList && - (exceptions.length === 0 || exceptions != null) - ) { - handleGetTotals(); - dispatch({ - type: 'updateIsInitLoading', - loading: false, - }); - - setLastUpdated(Date.now()); + if (!canUserCRUD || !hasIndexWrite) { + setReadOnly(true); + } else { + setReadOnly(false); } - }, [handleGetTotals, isInitLoading, exceptions, loadingList, dispatch, isLoadingReferences]); - - const showEmpty: boolean = !isInitLoading && !loadingList && exceptions.length === 0; + }, [setReadOnly, canUserCRUD, hasIndexWrite]); - const showNoResults: boolean = - exceptions.length === 0 && (totalEndpointItems > 0 || totalDetectionsItems > 0); + useEffect(() => { + if (exceptionLists.length > 0) { + handleGetExceptionListItems(); + } else { + setViewerState('loading'); + } + }, [exceptionLists, handleGetExceptionListItems, setViewerState]); return ( <> @@ -348,8 +342,8 @@ const ExceptionsViewerComponent = ({ dataViewId={rule.data_view_id} exceptionListType={exceptionListTypeToEdit} exceptionItem={exceptionToEdit} - onCancel={handleOnCancelExceptionModal} - onConfirm={handleOnConfirmExceptionModal} + onCancel={handleCancelExceptionItemFlyout} + onConfirm={handleConfirmExceptionFlyout} onRuleChange={onRuleChange} /> )} @@ -361,53 +355,46 @@ const ExceptionsViewerComponent = ({ dataViewId={rule.data_view_id} ruleId={rule.id} exceptionListType={exceptionListTypeToEdit} - onCancel={handleOnCancelExceptionModal} - onConfirm={handleOnConfirmExceptionModal} + onCancel={handleCancelExceptionItemFlyout} + onConfirm={handleConfirmExceptionFlyout} onRuleChange={onRuleChange} /> )} - {isInitLoading || loadingList || isLoadingReferences ? ( - + {viewerState != null ? ( + ) : ( <> - {!showEmpty && ( - <> - - - - - - - )} + + + + + - {!showEmpty && ( - - )} + )} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts index 0caf7e9cd384f..91691dce9ab5e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts @@ -10,53 +10,41 @@ import type { ExceptionListItemSchema, ExceptionListIdentifiers, Pagination, + ListArray, } from '@kbn/securitysolution-io-ts-list-types'; -import type { - FilterOptions, - ExceptionsPagination, - ExceptionListItemIdentifiers, - Filter, -} from '../types'; +import type { ExceptionsPagination, ExceptionListItemIdentifiers } from '../types'; export type ViewerFlyoutName = 'addException' | 'editException' | null; +export type ViewerState = 'error' | 'empty' | 'empty_search' | 'loading' | 'searching' | null; export interface State { - filterOptions: FilterOptions; pagination: ExceptionsPagination; exceptions: ExceptionListItemSchema[]; exceptionToEdit: ExceptionListItemSchema | null; loadingItemIds: ExceptionListItemIdentifiers[]; - isInitLoading: boolean; currentModal: ViewerFlyoutName; exceptionListTypeToEdit: ExceptionListType | null; - totalEndpointItems: number; - totalDetectionsItems: number; + viewerState: ViewerState; + exceptionLists: ListArray; } export type Action = | { type: 'setExceptions'; - lists: ExceptionListIdentifiers[]; exceptions: ExceptionListItemSchema[]; pagination: Pagination; } - | { - type: 'updateFilterOptions'; - filters: Partial; - } - | { type: 'updateIsInitLoading'; loading: boolean } - | { type: 'updateModalOpen'; modalName: ViewerFlyoutName } + | { type: 'updateFlyoutOpen'; flyoutType: ViewerFlyoutName } | { type: 'updateExceptionToEdit'; - lists: ExceptionListIdentifiers[]; + lists: ListArray; exception: ExceptionListItemSchema; } | { type: 'updateLoadingItemIds'; items: ExceptionListItemIdentifiers[] } | { type: 'updateExceptionListTypeToEdit'; exceptionListType: ExceptionListType | null } | { - type: 'setExceptionItemTotals'; - totalEndpointItems: number | null; - totalDetectionsItems: number | null; + type: 'setViewerState'; + state: ViewerState; }; export const allExceptionItemsReducer = @@ -77,39 +65,6 @@ export const allExceptionItemsReducer = exceptions, }; } - case 'updateFilterOptions': { - const { filter, pagination } = action.filters; - return { - ...state, - filterOptions: { - ...state.filterOptions, - ...filter, - }, - pagination: { - ...state.pagination, - ...pagination, - }, - }; - } - case 'setExceptionItemTotals': { - return { - ...state, - totalEndpointItems: - action.totalEndpointItems == null - ? state.totalEndpointItems - : action.totalEndpointItems, - totalDetectionsItems: - action.totalDetectionsItems == null - ? state.totalDetectionsItems - : action.totalDetectionsItems, - }; - } - case 'updateIsInitLoading': { - return { - ...state, - isInitLoading: action.loading, - }; - } case 'updateLoadingItemIds': { return { ...state, @@ -119,7 +74,7 @@ export const allExceptionItemsReducer = case 'updateExceptionToEdit': { const { exception, lists } = action; const exceptionListToEdit = lists.find((list) => { - return list !== null && exception.list_id === list.listId; + return list !== null && exception.list_id === list.list_id; }); return { ...state, @@ -127,10 +82,10 @@ export const allExceptionItemsReducer = exceptionListTypeToEdit: exceptionListToEdit ? exceptionListToEdit.type : null, }; } - case 'updateModalOpen': { + case 'updateFlyoutOpen': { return { ...state, - currentModal: action.modalName, + currentModal: action.flyoutType, }; } case 'updateExceptionListTypeToEdit': { @@ -139,6 +94,12 @@ export const allExceptionItemsReducer = exceptionListTypeToEdit: action.exceptionListType, }; } + case 'setViewerState': { + return { + ...state, + viewerState: action.state, + }; + } default: return state; } diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/translations.ts index 40d8c0c8a31ce..687945bfb1c16 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/translations.ts @@ -7,10 +7,17 @@ import { i18n } from '@kbn/i18n'; +export const EXCEPTION_NO_SEARCH_RESULTS_PROMPT_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.noSearchResultsPromptTitle', + { + defaultMessage: 'No results', + } +); + export const EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY = i18n.translate( 'xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody', { - defaultMessage: 'No search results found', + defaultMessage: 'No matching exception items were found in your search.', } ); @@ -56,3 +63,31 @@ export const EXCEPTION_LOADING_TITLE = i18n.translate( defaultMessage: 'Loading exceptions', } ); + +export const EXCEPTION_ERROR_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.exceptionItemsFetchError', + { + defaultMessage: 'Unable to load exception items', + } +); + +export const EXCEPTION_ERROR_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.exceptionItemsFetchErrorDescription', + { + defaultMessage: 'There was an error loading the exception items. Contact your administrator for help.', + } +); + +export const EXCEPTION_SEARCH_ERROR_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.exceptionItemSearchErrorTitle', + { + defaultMessage: 'Error searching', + } +); + +export const EXCEPTION_SEARCH_ERROR_BODY = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.exceptionItemSearchErrorBody', + { + defaultMessage: 'An error occurred searching for exception items. Please try again.', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/use_initial_exceptions_viewer_render.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/use_initial_exceptions_viewer_render.tsx new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index b5357cf03cf50..22a9c4149b36b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -388,12 +388,15 @@ export const findRuleExceptionReferences = async ({ http, signal, }: FindRulesReferencedByExceptionsProps): Promise => - http.fetch(`${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, { - method: 'GET', - query: { - ids: lists.map(({ id }) => id).join(','), - list_ids: lists.map(({ listId }) => listId).join(','), - namespace_types: lists.map(({ namespaceType }) => namespaceType).join(','), - }, - signal, - }); + http.fetch( + `${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, + { + method: 'GET', + query: { + ids: lists.map(({ id }) => id).join(','), + list_ids: lists.map(({ listId }) => listId).join(','), + namespace_types: lists.map(({ namespaceType }) => namespaceType).join(','), + }, + signal, + } + ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 593895d98e3cf..852c7d44aab97 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -28,7 +28,6 @@ import { useParams } from 'react-router-dom'; import type { ConnectedProps } from 'react-redux'; import { connect, useDispatch } from 'react-redux'; import styled from 'styled-components'; -import type { ExceptionListIdentifiers } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import type { Dispatch } from 'redux'; @@ -224,31 +223,19 @@ const RuleDetailsPageComponent: React.FC = ({ const [rule, setRule] = useState(null); const isLoading = ruleLoading && rule == null; - const exceptionLists = useMemo((): { - lists: ExceptionListIdentifiers[]; - allowedExceptionListTypes: ExceptionListTypeEnum[]; - } => { + const allowedExceptionListTypes = useMemo((): ExceptionListTypeEnum[] => { if (rule != null && rule.exceptions_list != null) { - return rule.exceptions_list.reduce<{ - lists: ExceptionListIdentifiers[]; - allowedExceptionListTypes: ExceptionListTypeEnum[]; - }>( - (acc, { id, list_id: listId, namespace_type: namespaceType, type }) => { - const { allowedExceptionListTypes, lists } = acc; + return rule.exceptions_list.reduce( + (acc, { type }) => { const shouldAddEndpoint = type === ExceptionListTypeEnum.ENDPOINT && - !allowedExceptionListTypes.includes(ExceptionListTypeEnum.ENDPOINT); - return { - lists: [...lists, { id, listId, namespaceType, type }], - allowedExceptionListTypes: shouldAddEndpoint - ? [...allowedExceptionListTypes, ExceptionListTypeEnum.ENDPOINT] - : allowedExceptionListTypes, - }; + !acc.includes(ExceptionListTypeEnum.ENDPOINT); + return shouldAddEndpoint ? [...acc, ExceptionListTypeEnum.ENDPOINT] : acc; }, - { lists: [], allowedExceptionListTypes: [ExceptionListTypeEnum.DETECTION] } + [ExceptionListTypeEnum.DETECTION] ); } else { - return { lists: [], allowedExceptionListTypes: [ExceptionListTypeEnum.DETECTION] }; + return [ExceptionListTypeEnum.DETECTION]; } }, [rule]); @@ -283,7 +270,7 @@ const RuleDetailsPageComponent: React.FC = ({ ); const ruleDetailTabs = useMemo(() => { - return exceptionLists.allowedExceptionListTypes.includes(ExceptionListTypeEnum.ENDPOINT) + return allowedExceptionListTypes.includes(ExceptionListTypeEnum.ENDPOINT) ? [ ...ruleDetailTabsDefault, { @@ -294,7 +281,7 @@ const RuleDetailsPageComponent: React.FC = ({ }, ] : ruleDetailTabsDefault; - }, [ruleDetailTabsDefault, exceptionLists]); + }, [ruleDetailTabsDefault, allowedExceptionListTypes]); const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.alerts); const [pageTabs, setTabs] = useState(ruleDetailTabs); @@ -884,7 +871,6 @@ const RuleDetailsPageComponent: React.FC = ({ @@ -893,7 +879,6 @@ const RuleDetailsPageComponent: React.FC = ({ diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 73ed2c6c3ddde..4b2d082907520 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -25328,7 +25328,6 @@ "xpack.securitySolution.detectionEngine.ruleDetails.backToRulesButton": "Règles", "xpack.securitySolution.detectionEngine.ruleDetails.deletedRule": "Règle supprimée", "xpack.securitySolution.detectionEngine.ruleDetails.enableRuleLabel": "Activer", - "xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab": "Exceptions", "xpack.securitySolution.detectionEngine.ruleDetails.pageTitle": "Détails de la règle", "xpack.securitySolution.detectionEngine.ruleDetails.ruleCreationDescription": "Créé par : {by} le {date}", "xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.actionFieldNotFoundErrorDescription": "Impossible de trouver le champ \"kibana.alert.rule.execution.uuid\" dans l'index des alertes.", @@ -26267,17 +26266,12 @@ "xpack.securitySolution.exceptions.addException.operatingSystemPlaceHolder": "Sélectionner un système d'exploitation", "xpack.securitySolution.exceptions.addException.sequenceWarning": "La requête de cette règle contient une instruction de séquence EQL. L'exception créée s'appliquera à tous les événements de la séquence.", "xpack.securitySolution.exceptions.addException.success": "Exception ajoutée avec succès", - "xpack.securitySolution.exceptions.andDescription": "AND", "xpack.securitySolution.exceptions.badge.readOnly.tooltip": "Impossible de créer, de modifier ou de supprimer des exceptions", "xpack.securitySolution.exceptions.cancelLabel": "Annuler", "xpack.securitySolution.exceptions.clearExceptionsLabel": "Retirer la liste d'exceptions", "xpack.securitySolution.exceptions.commentEventLabel": "a ajouté un commentaire", - "xpack.securitySolution.exceptions.commentLabel": "Commentaire", - "xpack.securitySolution.exceptions.descriptionLabel": "Description", - "xpack.securitySolution.exceptions.detectionListLabel": "Liste de détection", "xpack.securitySolution.exceptions.dissasociateExceptionListError": "Impossible de retirer la liste d'exceptions", "xpack.securitySolution.exceptions.dissasociateListSuccessText": "La liste d'exceptions ({id}) a été retirée avec succès", - "xpack.securitySolution.exceptions.editButtonLabel": "Modifier", "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "Fermer toutes les alertes qui correspondent à cette exception et ont été générées par cette règle", "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "Fermer toutes les alertes qui correspondent à cette exception et ont été générées par cette règle (les listes et les champs non ECS ne sont pas pris en charge)", "xpack.securitySolution.exceptions.editException.cancel": "Annuler", @@ -26290,47 +26284,31 @@ "xpack.securitySolution.exceptions.editException.success": "L'exception a été mise à jour avec succès", "xpack.securitySolution.exceptions.editException.versionConflictDescription": "Cette exception semble avoir été mise à jour depuis que vous l'avez sélectionnée pour la modifier. Essayez de cliquer sur \"Annuler\" et de modifier à nouveau l'exception.", "xpack.securitySolution.exceptions.editException.versionConflictTitle": "Désolé, une erreur est survenue", - "xpack.securitySolution.exceptions.endpointListLabel": "Liste de points de terminaison", "xpack.securitySolution.exceptions.errorLabel": "Erreur", - "xpack.securitySolution.exceptions.exceptionsPaginationLabel": "Éléments par page : {items}", "xpack.securitySolution.exceptions.failedLoadPolicies": "Une erreur s'est produite lors du chargement des politiques : \"{error}\"", "xpack.securitySolution.exceptions.fetch404Error": "La liste d'exceptions associée ({listId}) n'existe plus. Veuillez retirer la liste d'exceptions manquante pour ajouter des exceptions supplémentaires à la règle de détection.", "xpack.securitySolution.exceptions.fetchError": "Erreur lors de la récupération de la liste d'exceptions", - "xpack.securitySolution.exceptions.fieldDescription": "Champ", "xpack.securitySolution.exceptions.hideCommentsLabel": "Masquer ({comments}) {comments, plural, =1 {commentaire} other {commentaires}}", "xpack.securitySolution.exceptions.modalErrorAccordionText": "Afficher les informations de référence de la règle :", - "xpack.securitySolution.exceptions.nameLabel": "Nom", "xpack.securitySolution.exceptions.operatingSystemFullLabel": "Système d'exploitation", "xpack.securitySolution.exceptions.operatingSystemLinux": "Linux", "xpack.securitySolution.exceptions.operatingSystemMac": "macOS", "xpack.securitySolution.exceptions.operatingSystemWindows": "Windows", "xpack.securitySolution.exceptions.operatingSystemWindowsAndMac": "Windows et macOS", - "xpack.securitySolution.exceptions.operatorDescription": "Opérateur", - "xpack.securitySolution.exceptions.orDescription": "OR", "xpack.securitySolution.exceptions.paginationNumberOfItemsLabel": "{items} éléments", "xpack.securitySolution.exceptions.referenceModalCancelButton": "Annuler", "xpack.securitySolution.exceptions.referenceModalDeleteButton": "Retirer la liste d'exceptions", "xpack.securitySolution.exceptions.referenceModalDescription": "Cette liste d'exceptions est associée à ({referenceCount}) {referenceCount, plural, =1 {règle} other {règles}}. Le retrait de cette liste d'exceptions supprimera également sa référence des règles associées.", "xpack.securitySolution.exceptions.referenceModalSuccessDescription": "Liste d'exceptions - {listId} - supprimée avec succès.", "xpack.securitySolution.exceptions.referenceModalTitle": "Retirer la liste d'exceptions", - "xpack.securitySolution.exceptions.removeButtonLabel": "Retirer", "xpack.securitySolution.exceptions.searchPlaceholder": "par ex. Exemple de liste de noms", "xpack.securitySolution.exceptions.showCommentsLabel": "Afficher ({comments}) {comments, plural, =1 {commentaire} other {commentaires}}", - "xpack.securitySolution.exceptions.utilityNumberExceptionsLabel": "Affichage de {items} {items, plural, =1 {exception} other {exceptions}}", - "xpack.securitySolution.exceptions.utilityRefreshLabel": "Actualiser", - "xpack.securitySolution.exceptions.valueDescription": "Valeur", "xpack.securitySolution.exceptions.viewer.addCommentPlaceholder": "Ajouter un nouveau commentaire...", - "xpack.securitySolution.exceptions.viewer.addExceptionLabel": "Ajouter une nouvelle exception", "xpack.securitySolution.exceptions.viewer.addToClipboard": "Commentaire", "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "Ajouter une exception à une règle", "xpack.securitySolution.exceptions.viewer.addToEndpointListLabel": "Ajouter une exception de point de terminaison", "xpack.securitySolution.exceptions.viewer.deleteExceptionError": "Erreur lors de la suppression de l'exception", "xpack.securitySolution.exceptions.viewer.emptyPromptBody": "Vous pouvez ajouter des exceptions pour affiner la règle de façon à ce que les alertes de détection ne soient pas créées lorsque les conditions d'exception sont remplies. Les exceptions améliorent la précision de la détection, ce qui peut permettre de réduire le nombre de faux positifs.", - "xpack.securitySolution.exceptions.viewer.emptyPromptTitle": "Cette règle ne possède aucune exception", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription": "Toutes les exceptions à cette règle sont appliquées à la règle de détection, et non au point de terminaison. Afficher les {ruleSettings} pour plus de détails.", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription.ruleSettingsLink": "paramètres de règles", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription": "Toutes les exceptions à cette règle sont appliquées au point de terminaison et à la règle de détection. Afficher les {ruleSettings} pour plus de détails.", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription.ruleSettingsLink": "paramètres de règles", "xpack.securitySolution.exceptions.viewer.fetchingListError": "Erreur lors de la récupération des exceptions", "xpack.securitySolution.exceptions.viewer.fetchTotalsError": "Erreur lors de l'obtention des totaux d'éléments de l'exception", "xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody": "Aucun résultat n'a été trouvé pour la recherche.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0e1dd9aa10e84..118d45e7bcc26 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25406,7 +25406,6 @@ "xpack.securitySolution.detectionEngine.ruleDetails.backToRulesButton": "ルール", "xpack.securitySolution.detectionEngine.ruleDetails.deletedRule": "削除されたルール", "xpack.securitySolution.detectionEngine.ruleDetails.enableRuleLabel": "有効にする", - "xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab": "例外", "xpack.securitySolution.detectionEngine.ruleDetails.pageTitle": "ルール詳細", "xpack.securitySolution.detectionEngine.ruleDetails.ruleCreationDescription": "作成者:{by} 日付:{date}", "xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.actionFieldNotFoundErrorDescription": "アラートインデックスにフィールド'kibana.alert.rule.execution.uuid'が見つかりません。", @@ -26345,17 +26344,12 @@ "xpack.securitySolution.exceptions.addException.operatingSystemPlaceHolder": "オペレーティングシステムを選択", "xpack.securitySolution.exceptions.addException.sequenceWarning": "このルールのクエリにはEQLシーケンス文があります。作成された例外は、シーケンスのすべてのイベントに適用されます。", "xpack.securitySolution.exceptions.addException.success": "正常に例外を追加しました", - "xpack.securitySolution.exceptions.andDescription": "AND", "xpack.securitySolution.exceptions.badge.readOnly.tooltip": "例外を作成、編集、削除できません", "xpack.securitySolution.exceptions.cancelLabel": "キャンセル", "xpack.securitySolution.exceptions.clearExceptionsLabel": "例外リストを削除", "xpack.securitySolution.exceptions.commentEventLabel": "コメントを追加しました", - "xpack.securitySolution.exceptions.commentLabel": "コメント", - "xpack.securitySolution.exceptions.descriptionLabel": "説明", - "xpack.securitySolution.exceptions.detectionListLabel": "検出リスト", "xpack.securitySolution.exceptions.dissasociateExceptionListError": "例外リストを削除できませんでした", "xpack.securitySolution.exceptions.dissasociateListSuccessText": "例外リスト({id})が正常に削除されました", - "xpack.securitySolution.exceptions.editButtonLabel": "編集", "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "この例外一致し、このルールによって生成された、すべてのアラートを閉じる", "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "この例外と一致し、このルールによって生成された、すべてのアラートを閉じる(リストと非ECSフィールドはサポートされません)", "xpack.securitySolution.exceptions.editException.cancel": "キャンセル", @@ -26368,47 +26362,31 @@ "xpack.securitySolution.exceptions.editException.success": "正常に例外を更新しました", "xpack.securitySolution.exceptions.editException.versionConflictDescription": "最初に編集することを選択したときからこの例外が更新されている可能性があります。[キャンセル]をクリックし、もう一度例外を編集してください。", "xpack.securitySolution.exceptions.editException.versionConflictTitle": "申し訳ございません、エラーが発生しました", - "xpack.securitySolution.exceptions.endpointListLabel": "エンドポイントリスト", "xpack.securitySolution.exceptions.errorLabel": "エラー", - "xpack.securitySolution.exceptions.exceptionsPaginationLabel": "ページごとの項目数:{items}", "xpack.securitySolution.exceptions.failedLoadPolicies": "ポリシーの読み込みエラーが発生しました:\"{error}\"", "xpack.securitySolution.exceptions.fetch404Error": "関連付けられた例外リスト({listId})は存在しません。その他の例外を検出ルールに追加するには、見つからない例外リストを削除してください。", "xpack.securitySolution.exceptions.fetchError": "例外リストの取得エラー", - "xpack.securitySolution.exceptions.fieldDescription": "フィールド", "xpack.securitySolution.exceptions.hideCommentsLabel": "({comments}){comments, plural, other {件のコメント}}を非表示", "xpack.securitySolution.exceptions.modalErrorAccordionText": "ルール参照情報を表示:", - "xpack.securitySolution.exceptions.nameLabel": "名前", "xpack.securitySolution.exceptions.operatingSystemFullLabel": "オペレーティングシステム", "xpack.securitySolution.exceptions.operatingSystemLinux": "Linux", "xpack.securitySolution.exceptions.operatingSystemMac": "macOS", "xpack.securitySolution.exceptions.operatingSystemWindows": "Windows", "xpack.securitySolution.exceptions.operatingSystemWindowsAndMac": "WindowsおよびmacOS", - "xpack.securitySolution.exceptions.operatorDescription": "演算子", - "xpack.securitySolution.exceptions.orDescription": "OR", "xpack.securitySolution.exceptions.paginationNumberOfItemsLabel": "{items}個の項目", "xpack.securitySolution.exceptions.referenceModalCancelButton": "キャンセル", "xpack.securitySolution.exceptions.referenceModalDeleteButton": "例外リストを削除", "xpack.securitySolution.exceptions.referenceModalDescription": "この例外リストは、({referenceCount}) {referenceCount, plural, other {個のルール}}に関連付けられています。この例外リストを削除すると、関連付けられたルールからの参照も削除されます。", "xpack.securitySolution.exceptions.referenceModalSuccessDescription": "例外リスト{listId}が正常に削除されました。", "xpack.securitySolution.exceptions.referenceModalTitle": "例外リストを削除", - "xpack.securitySolution.exceptions.removeButtonLabel": "削除", "xpack.securitySolution.exceptions.searchPlaceholder": "例:例外リスト名", "xpack.securitySolution.exceptions.showCommentsLabel": "({comments}){comments, plural, other {件のコメント}}を表示", - "xpack.securitySolution.exceptions.utilityNumberExceptionsLabel": "{items} {items, plural, other {件の例外}}を表示しています", - "xpack.securitySolution.exceptions.utilityRefreshLabel": "更新", - "xpack.securitySolution.exceptions.valueDescription": "値", "xpack.securitySolution.exceptions.viewer.addCommentPlaceholder": "新しいコメントを追加...", - "xpack.securitySolution.exceptions.viewer.addExceptionLabel": "新しい例外を追加", "xpack.securitySolution.exceptions.viewer.addToClipboard": "コメント", "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "ルール例外の追加", "xpack.securitySolution.exceptions.viewer.addToEndpointListLabel": "エンドポイント例外の追加", "xpack.securitySolution.exceptions.viewer.deleteExceptionError": "例外の削除エラー", "xpack.securitySolution.exceptions.viewer.emptyPromptBody": "例外を追加してルールを微調整し、例外条件が満たされたときに検出アラートが作成されないようにすることができます。例外により検出の精度が改善します。これにより、誤検出数が減ります。", - "xpack.securitySolution.exceptions.viewer.emptyPromptTitle": "このルールには例外がありません", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription": "このルールのすべての例外は、エンドポイントではなく、検出ルールに適用されます。詳細については、{ruleSettings}を確認してください。", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription.ruleSettingsLink": "ルール設定", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription": "このルールのすべての例外は、エンドポイントと検出ルールに適用されます。詳細については、{ruleSettings}を確認してください。", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription.ruleSettingsLink": "ルール設定", "xpack.securitySolution.exceptions.viewer.fetchingListError": "例外の取得エラー", "xpack.securitySolution.exceptions.viewer.fetchTotalsError": "例外項目合計数の取得エラー", "xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody": "検索結果が見つかりません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8d60dd7376dce..cfeb8b3881f05 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25431,7 +25431,6 @@ "xpack.securitySolution.detectionEngine.ruleDetails.backToRulesButton": "规则", "xpack.securitySolution.detectionEngine.ruleDetails.deletedRule": "已删除规则", "xpack.securitySolution.detectionEngine.ruleDetails.enableRuleLabel": "启用", - "xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab": "例外", "xpack.securitySolution.detectionEngine.ruleDetails.pageTitle": "规则详情", "xpack.securitySolution.detectionEngine.ruleDetails.ruleCreationDescription": "由 {by} 于 {date}创建", "xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.actionFieldNotFoundErrorDescription": "在告警索引中找不到字段“kibana.alert.rule.execution.uuid”。", @@ -26371,17 +26370,12 @@ "xpack.securitySolution.exceptions.addException.operatingSystemPlaceHolder": "选择操作系统", "xpack.securitySolution.exceptions.addException.sequenceWarning": "此规则的查询包含 EQL 序列语句。创建的例外将应用于序列中的所有事件。", "xpack.securitySolution.exceptions.addException.success": "已成功添加例外", - "xpack.securitySolution.exceptions.andDescription": "且", "xpack.securitySolution.exceptions.badge.readOnly.tooltip": "无法创建、编辑或删除例外", "xpack.securitySolution.exceptions.cancelLabel": "取消", "xpack.securitySolution.exceptions.clearExceptionsLabel": "移除例外列表", "xpack.securitySolution.exceptions.commentEventLabel": "已添加注释", - "xpack.securitySolution.exceptions.commentLabel": "注释", - "xpack.securitySolution.exceptions.descriptionLabel": "描述", - "xpack.securitySolution.exceptions.detectionListLabel": "检测列表", "xpack.securitySolution.exceptions.dissasociateExceptionListError": "无法移除例外列表", "xpack.securitySolution.exceptions.dissasociateListSuccessText": "例外列表 ({id}) 已成功移除", - "xpack.securitySolution.exceptions.editButtonLabel": "编辑", "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "关闭所有与此例外匹配且根据此规则生成的告警", "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "关闭所有与此例外匹配且根据此规则生成的告警(不支持列表和非 ECS 字段)", "xpack.securitySolution.exceptions.editException.cancel": "取消", @@ -26394,47 +26388,31 @@ "xpack.securitySolution.exceptions.editException.success": "已成功更新例外", "xpack.securitySolution.exceptions.editException.versionConflictDescription": "此例外可能自您首次选择编辑后已更新。尝试单击“取消”,重新编辑该例外。", "xpack.securitySolution.exceptions.editException.versionConflictTitle": "抱歉,有错误", - "xpack.securitySolution.exceptions.endpointListLabel": "终端列表", "xpack.securitySolution.exceptions.errorLabel": "错误", - "xpack.securitySolution.exceptions.exceptionsPaginationLabel": "每页项数:{items}", "xpack.securitySolution.exceptions.failedLoadPolicies": "加载策略时出错:“{error}”", "xpack.securitySolution.exceptions.fetch404Error": "关联的例外列表 ({listId}) 已不存在。请移除缺少的例外列表,以将其他例外添加到检测规则。", "xpack.securitySolution.exceptions.fetchError": "提取例外列表时出错", - "xpack.securitySolution.exceptions.fieldDescription": "字段", "xpack.securitySolution.exceptions.hideCommentsLabel": "隐藏 ({comments}) 个{comments, plural, other {注释}}", "xpack.securitySolution.exceptions.modalErrorAccordionText": "显示规则引用信息:", - "xpack.securitySolution.exceptions.nameLabel": "名称", "xpack.securitySolution.exceptions.operatingSystemFullLabel": "操作系统", "xpack.securitySolution.exceptions.operatingSystemLinux": "Linux", "xpack.securitySolution.exceptions.operatingSystemMac": "macOS", "xpack.securitySolution.exceptions.operatingSystemWindows": "Windows", "xpack.securitySolution.exceptions.operatingSystemWindowsAndMac": "Windows 和 macOS", - "xpack.securitySolution.exceptions.operatorDescription": "运算符", - "xpack.securitySolution.exceptions.orDescription": "或", "xpack.securitySolution.exceptions.paginationNumberOfItemsLabel": "{items} 项", "xpack.securitySolution.exceptions.referenceModalCancelButton": "取消", "xpack.securitySolution.exceptions.referenceModalDeleteButton": "移除例外列表", "xpack.securitySolution.exceptions.referenceModalDescription": "此例外列表与 ({referenceCount}) 个{referenceCount, plural, other {规则}}关联。移除此例外列表还将会删除其对关联规则的引用。", "xpack.securitySolution.exceptions.referenceModalSuccessDescription": "例外列表 - {listId} - 已成功删除。", "xpack.securitySolution.exceptions.referenceModalTitle": "移除例外列表", - "xpack.securitySolution.exceptions.removeButtonLabel": "移除", "xpack.securitySolution.exceptions.searchPlaceholder": "例如,示例列表名称", "xpack.securitySolution.exceptions.showCommentsLabel": "显示 ({comments} 个) {comments, plural, other {注释}}", - "xpack.securitySolution.exceptions.utilityNumberExceptionsLabel": "正在显示 {items} 个{items, plural, other {例外}}", - "xpack.securitySolution.exceptions.utilityRefreshLabel": "刷新", - "xpack.securitySolution.exceptions.valueDescription": "值", "xpack.securitySolution.exceptions.viewer.addCommentPlaceholder": "添加新注释......", - "xpack.securitySolution.exceptions.viewer.addExceptionLabel": "添加新例外", "xpack.securitySolution.exceptions.viewer.addToClipboard": "注释", "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "添加规则例外", "xpack.securitySolution.exceptions.viewer.addToEndpointListLabel": "添加终端例外", "xpack.securitySolution.exceptions.viewer.deleteExceptionError": "删除例外时出错", "xpack.securitySolution.exceptions.viewer.emptyPromptBody": "可以添加例外以微调规则,以便在满足例外条件时不创建检测告警。例外提升检测精确性,从而可以减少误报数。", - "xpack.securitySolution.exceptions.viewer.emptyPromptTitle": "此规则没有例外", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription": "此规则的所有例外将应用到检测规则,而非终端。查看{ruleSettings}以了解详情。", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription.ruleSettingsLink": "规则设置", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription": "此规则的所有例外将应用到终端和检测规则。查看{ruleSettings}以了解详情。", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription.ruleSettingsLink": "规则设置", "xpack.securitySolution.exceptions.viewer.fetchingListError": "提取例外时出错", "xpack.securitySolution.exceptions.viewer.fetchTotalsError": "获取例外项总数时出错", "xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody": "找不到搜索结果。", diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rule_exception_references.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rule_exception_references.ts index e762a9e016486..f6ea0f92970db 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rule_exception_references.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rule_exception_references.ts @@ -29,7 +29,7 @@ export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const log = getService('log'); - describe.only('find_rule_exception_references', () => { + describe('find_rule_exception_references', () => { before(async () => { await createSignalsIndex(supertest, log); }); From 07cbd36e9649f6ef5945284a60a10a46137aa9e6 Mon Sep 17 00:00:00 2001 From: yctercero Date: Thu, 25 Aug 2022 08:13:28 -0700 Subject: [PATCH 07/30] moving components under new folder structure --- .../src/common/search/index.test.ts | 56 ++ .../src/common/search/index.ts | 6 +- .../index.mock.ts | 4 + .../index.test.ts | 15 +- .../src/typescript_types/index.ts | 4 +- .../src/api/index.ts | 10 +- .../src/use_api/index.ts | 4 +- .../lists/public/exceptions/api.test.ts | 90 +--- .../public/exceptions/hooks/use_api.test.ts | 4 - .../hooks/use_exception_list_items.test.ts | 498 ------------------ .../find_exception_list_items.ts | 2 +- .../viewer/exceptions_pagination.test.tsx | 142 ----- .../viewer/exceptions_viewer_items.tsx | 75 --- .../exceptions/viewer/no_exception_items.tsx | 62 --- .../exceptions/viewer/no_search_results.tsx | 33 -- .../use_initial_exceptions_viewer_render.tsx | 0 .../add_exception_flyout/index.test.tsx | 20 +- .../add_exception_flyout/index.tsx | 24 +- .../add_exception_flyout/translations.ts | 0 .../all_items.test.tsx} | 57 +- .../all_exception_items_table/all_items.tsx | 91 ++++ .../empty_viewer_state.test.tsx | 84 +++ .../empty_viewer_state.tsx | 22 +- .../all_exception_items_table}/index.test.tsx | 110 ++-- .../all_exception_items_table}/index.tsx | 75 +-- .../pagination.test.tsx | 71 +++ .../all_exception_items_table/pagination.tsx} | 6 +- .../all_exception_items_table}/reducer.ts | 3 +- .../search_bar.test.tsx} | 62 +-- .../all_exception_items_table/search_bar.tsx} | 14 +- .../translations.ts | 41 +- .../utility_bar.test.tsx} | 8 +- .../utility_bar.tsx} | 18 +- .../edit_exception_flyout/index.test.tsx | 16 +- .../edit_exception_flyout/index.tsx | 18 +- .../edit_exception_flyout/translations.ts | 0 .../components/error_callout/index.test.tsx} | 14 +- .../components/error_callout/index.tsx} | 6 +- .../exception_item_card/comments.tsx | 48 ++ .../exception_item_card/conditions.test.tsx} | 4 +- .../exception_item_card/conditions.tsx} | 0 .../exception_item_card/header.test.tsx} | 4 +- .../exception_item_card/header.tsx} | 0 .../exception_item_card/index.test.tsx | 4 +- .../components}/exception_item_card/index.tsx | 12 +- .../exception_item_card/meta.test.tsx} | 4 +- .../components/exception_item_card/meta.tsx} | 14 +- .../exception_item_card/translations.ts | 0 .../components/item_comments/index.tsx} | 16 +- .../logic}/use_add_exception.test.tsx | 6 +- .../logic}/use_add_exception.tsx | 4 +- ...tch_or_create_rule_exception_list.test.tsx | 0 ...se_fetch_or_create_rule_exception_list.tsx | 0 .../logic}/use_find_references.tsx | 8 +- .../utils}/exceptionable_endpoint_fields.json | 0 .../utils}/exceptionable_linux_fields.json | 0 .../exceptionable_windows_mac_fields.json | 0 .../rule_exceptions/utils}/helpers.test.tsx | 0 .../rule_exceptions/utils}/helpers.tsx | 2 +- .../rule_exceptions/utils}/translations.ts | 0 .../rule_exceptions/utils}/types.ts | 0 .../use_investigate_in_timeline.tsx | 1 - .../detection_engine/rules/details/index.tsx | 2 +- .../tests/find_exception_list_items.ts | 83 ++- 64 files changed, 733 insertions(+), 1244 deletions(-) create mode 100644 packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.test.ts delete mode 100644 x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.test.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/no_exception_items.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/no_search_results.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/use_initial_exceptions_viewer_render.tsx rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/components}/add_exception_flyout/index.test.tsx (96%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/components}/add_exception_flyout/index.tsx (96%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/components}/add_exception_flyout/translations.ts (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer/exceptions_viewer_items.test.tsx => detection_engine/rule_exceptions/components/all_exception_items_table/all_items.test.tsx} (57%) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.test.tsx rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components/all_exception_items_table}/empty_viewer_state.tsx (79%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components/all_exception_items_table}/index.test.tsx (53%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components/all_exception_items_table}/index.tsx (87%) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/pagination.test.tsx rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer/exceptions_pagination.tsx => detection_engine/rule_exceptions/components/all_exception_items_table/pagination.tsx} (88%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components/all_exception_items_table}/reducer.ts (98%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer/exceptions_viewer_header.test.tsx => detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.test.tsx} (55%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer/exceptions_viewer_header.tsx => detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx} (84%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components/all_exception_items_table}/translations.ts (57%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer/exceptions_utility.test.tsx => detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.test.tsx} (88%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer/exceptions_utility.tsx => detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.tsx} (82%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/components}/edit_exception_flyout/index.test.tsx (96%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/components}/edit_exception_flyout/index.tsx (96%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/components}/edit_exception_flyout/translations.ts (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/error_callout.test.tsx => detection_engine/rule_exceptions/components/error_callout/index.test.tsx} (90%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/error_callout.tsx => detection_engine/rule_exceptions/components/error_callout/index.tsx} (94%) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/comments.tsx rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.test.tsx => detection_engine/rule_exceptions/components/exception_item_card/conditions.test.tsx} (98%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx => detection_engine/rule_exceptions/components/exception_item_card/conditions.tsx} (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer/exception_item_card/exception_item_card_header.test.tsx => detection_engine/rule_exceptions/components/exception_item_card/header.test.tsx} (96%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer/exception_item_card/exception_item_card_header.tsx => detection_engine/rule_exceptions/components/exception_item_card/header.tsx} (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components}/exception_item_card/index.test.tsx (98%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components}/exception_item_card/index.tsx (91%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx => detection_engine/rule_exceptions/components/exception_item_card/meta.test.tsx} (97%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx => detection_engine/rule_exceptions/components/exception_item_card/meta.tsx} (90%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components}/exception_item_card/translations.ts (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/add_exception_comments.tsx => detection_engine/rule_exceptions/components/item_comments/index.tsx} (88%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/logic}/use_add_exception.test.tsx (98%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/logic}/use_add_exception.tsx (98%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/logic}/use_fetch_or_create_rule_exception_list.test.tsx (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/logic}/use_fetch_or_create_rule_exception_list.tsx (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/logic}/use_find_references.tsx (91%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/utils}/exceptionable_endpoint_fields.json (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/utils}/exceptionable_linux_fields.json (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/utils}/exceptionable_windows_mac_fields.json (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/utils}/helpers.test.tsx (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/utils}/helpers.tsx (99%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/utils}/translations.ts (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/utils}/types.ts (100%) diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.test.ts new file mode 100644 index 0000000000000..e18093ebcf0a5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.test.ts @@ -0,0 +1,56 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { exactCheck } from '@kbn/securitysolution-io-ts-utils'; +import { searchOrUndefined } from '.'; + +import * as t from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; + +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('searchAfter', () => { + test('it will validate a correct search', () => { + const payload = 'name:foo'; + const decoded = searchOrUndefined.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will validate with the value of "undefined"', () => { + const obj = t.exact( + t.type({ + search: searchOrUndefined, + }) + ); + const payload: t.TypeOf = { + search: undefined, + }; + const decoded = obj.decode({ + pit_id: undefined, + }); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will fail to validate an incorrect search', () => { + const payload = ['foo']; + const decoded = searchOrUndefined.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["foo"]" supplied to "(string | undefined)"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.ts index c65c4dca5acab..319c225edf007 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.ts @@ -9,5 +9,7 @@ import * as t from 'io-ts'; export const search = t.string; -export const searchFieldOrUndefined = t.union([search, t.undefined]); -export type SearchFieldOrUndefined = t.TypeOf; +export type Search = t.TypeOf; + +export const searchOrUndefined = t.union([search, t.undefined]); +export type SearchOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.mock.ts index 8f64dccf6d577..4026d878ca278 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.mock.ts @@ -16,6 +16,7 @@ export const getFindExceptionListItemSchemaMock = (): FindExceptionListItemSchem namespace_type: NAMESPACE_TYPE, page: '1', per_page: '25', + search: undefined, sort_field: undefined, sort_order: undefined, }); @@ -26,6 +27,7 @@ export const getFindExceptionListItemSchemaMultipleMock = (): FindExceptionListI namespace_type: 'single,single,agnostic', page: '1', per_page: '25', + search: undefined, sort_field: undefined, sort_order: undefined, }); @@ -37,6 +39,7 @@ export const getFindExceptionListItemSchemaDecodedMock = namespace_type: [NAMESPACE_TYPE], page: 1, per_page: 25, + search: undefined, sort_field: undefined, sort_order: undefined, }); @@ -48,6 +51,7 @@ export const getFindExceptionListItemSchemaDecodedMultipleMock = namespace_type: ['single', 'single', 'agnostic'], page: 1, per_page: 25, + search: undefined, sort_field: undefined, sort_order: undefined, }); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.test.ts index 04afee30c1ab3..fab9111aa0eb0 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.test.ts @@ -55,6 +55,7 @@ describe('find_list_item_schema', () => { namespace_type: ['single'], page: undefined, per_page: undefined, + search: undefined, sort_field: undefined, sort_order: undefined, }; @@ -73,7 +74,7 @@ describe('find_list_item_schema', () => { expect(message.schema).toEqual(expected); }); - test('it should validate with pre_page missing', () => { + test('it should validate with per_page missing', () => { const payload = getFindExceptionListItemSchemaMock(); delete payload.per_page; const decoded = findExceptionListItemSchema.decode(payload); @@ -123,6 +124,18 @@ describe('find_list_item_schema', () => { expect(message.schema).toEqual(expected); }); + test('it should validate with search missing', () => { + const payload = getFindExceptionListItemSchemaMock(); + delete payload.search; + const decoded = findExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindExceptionListItemSchemaDecodedMock(); + delete expected.search; + expect(message.schema).toEqual(expected); + }); + test('it should not allow an extra key to be sent in', () => { const payload: FindExceptionListItemSchema & { extraKey: string; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts index a6d065298c731..b8f03629135a2 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts @@ -99,10 +99,10 @@ export interface ExceptionListIdentifiers { export interface ApiCallFindListsItemsMemoProps { lists: ExceptionListIdentifiers[]; - filters?: string; pagination: Partial; showDetectionsListsOnly: boolean; showEndpointListsOnly: boolean; + filter?: string; onError: (arg: string[]) => void; onSuccess: (arg: UseExceptionListItemsSuccess) => void; } @@ -170,7 +170,7 @@ export interface ApiCallByListIdProps { namespaceTypes: NamespaceType[]; pagination: Partial; search?: string; - filters?: string; + filter?: string; signal: AbortSignal; } diff --git a/packages/kbn-securitysolution-list-api/src/api/index.ts b/packages/kbn-securitysolution-list-api/src/api/index.ts index 8443edcbb13eb..e33d911f40cf5 100644 --- a/packages/kbn-securitysolution-list-api/src/api/index.ts +++ b/packages/kbn-securitysolution-list-api/src/api/index.ts @@ -322,7 +322,7 @@ export { fetchExceptionListByIdWithValidation as fetchExceptionListById }; * @param http Kibana http service * @param listIds ExceptionList list_ids (not ID) * @param namespaceTypes ExceptionList namespace_types - * @param filterOptions optional - filter by field or tags + * @param filter optional * @param pagination optional * @param signal to cancel request * @@ -332,7 +332,7 @@ const fetchExceptionListsItemsByListIds = async ({ http, listIds, namespaceTypes, - filters, + filter, pagination, search, signal, @@ -345,7 +345,7 @@ const fetchExceptionListsItemsByListIds = async ({ search, sort_field: 'exception-list.created_at', sort_order: 'desc', - filter: filters, + filter, }; return http.fetch(`${EXCEPTION_LIST_ITEM_URL}/_find`, { @@ -356,7 +356,7 @@ const fetchExceptionListsItemsByListIds = async ({ }; const fetchExceptionListsItemsByListIdsWithValidation = async ({ - filters, + filter, http, listIds, namespaceTypes, @@ -369,7 +369,7 @@ const fetchExceptionListsItemsByListIdsWithValidation = async ({ tryCatch( () => fetchExceptionListsItemsByListIds({ - filters, + filter, http, listIds, namespaceTypes, diff --git a/packages/kbn-securitysolution-list-hooks/src/use_api/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_api/index.ts index a651bb0f088e3..ec76d169390cc 100644 --- a/packages/kbn-securitysolution-list-hooks/src/use_api/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/use_api/index.ts @@ -170,7 +170,7 @@ export const useApi = (http: HttpStart): ExceptionsApi => { }, async getExceptionListsItems({ lists, - filters, + filter, pagination, showDetectionsListsOnly, showEndpointListsOnly, @@ -192,7 +192,7 @@ export const useApi = (http: HttpStart): ExceptionsApi => { per_page: perPage, total, } = await Api.fetchExceptionListsItemsByListIds({ - filters, + filter, http, listIds: ids, namespaceTypes: namespaces, diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index a7d55139b7f5a..c6f423184f926 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -380,7 +380,6 @@ describe('Exceptions Lists API', () => { test('it invokes "fetchExceptionListsItemsByListIds" with expected url and body values', async () => { await fetchExceptionListsItemsByListIds({ - filterOptions: [], http: httpMock, listIds: ['myList', 'myOtherListId'], namespaceTypes: ['single', 'single'], @@ -405,14 +404,9 @@ describe('Exceptions Lists API', () => { }); }); - test('it invokes with expected url and body values when a filter exists and "namespaceType" of "single"', async () => { + test('it invokes with expected url and body values when a filter exists', async () => { await fetchExceptionListsItemsByListIds({ - filterOptions: [ - { - filter: 'hello world', - tags: [], - }, - ], + filter: 'exception-list.attributes.entries.field:hello world*', http: httpMock, listIds: ['myList'], namespaceTypes: ['single'], @@ -438,80 +432,8 @@ describe('Exceptions Lists API', () => { }); }); - test('it invokes with expected url and body values when a filter exists and "namespaceType" of "agnostic"', async () => { - await fetchExceptionListsItemsByListIds({ - filterOptions: [ - { - filter: 'hello world', - tags: [], - }, - ], - http: httpMock, - listIds: ['myList'], - namespaceTypes: ['agnostic'], - pagination: { - page: 1, - perPage: 20, - }, - signal: abortCtrl.signal, - }); - - expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items/_find', { - method: 'GET', - query: { - filter: 'exception-list-agnostic.attributes.entries.field:hello world*', - list_id: 'myList', - namespace_type: 'agnostic', - page: '1', - per_page: '20', - sort_field: 'exception-list.created_at', - sort_order: 'desc', - }, - signal: abortCtrl.signal, - }); - }); - - test('it invokes with expected url and body values when tags exists', async () => { + test('it invokes with expected url and body values when search exists', async () => { await fetchExceptionListsItemsByListIds({ - filterOptions: [ - { - filter: '', - tags: ['malware'], - }, - ], - http: httpMock, - listIds: ['myList'], - namespaceTypes: ['agnostic'], - pagination: { - page: 1, - perPage: 20, - }, - signal: abortCtrl.signal, - }); - - expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items/_find', { - method: 'GET', - query: { - filter: 'exception-list-agnostic.attributes.tags:malware', - list_id: 'myList', - namespace_type: 'agnostic', - page: '1', - per_page: '20', - sort_field: 'exception-list.created_at', - sort_order: 'desc', - }, - signal: abortCtrl.signal, - }); - }); - - test('it invokes with expected url and body values when filter and tags exists', async () => { - await fetchExceptionListsItemsByListIds({ - filterOptions: [ - { - filter: 'host.name', - tags: ['malware'], - }, - ], http: httpMock, listIds: ['myList'], namespaceTypes: ['agnostic'], @@ -519,18 +441,18 @@ describe('Exceptions Lists API', () => { page: 1, perPage: 20, }, + search: '-@timestamp', signal: abortCtrl.signal, }); expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items/_find', { method: 'GET', query: { - filter: - 'exception-list-agnostic.attributes.entries.field:host.name* AND exception-list-agnostic.attributes.tags:malware', list_id: 'myList', namespace_type: 'agnostic', page: '1', per_page: '20', + search: '-@timestamp', sort_field: 'exception-list.created_at', sort_order: 'desc', }, @@ -540,7 +462,6 @@ describe('Exceptions Lists API', () => { test('it returns expected format when call succeeds', async () => { const exceptionResponse = await fetchExceptionListsItemsByListIds({ - filterOptions: [], http: httpMock, listIds: ['endpoint_list_id'], namespaceTypes: ['single'], @@ -561,7 +482,6 @@ describe('Exceptions Lists API', () => { await expect( fetchExceptionListsItemsByListIds({ - filterOptions: [], http: httpMock, listIds: ['myList'], namespaceTypes: ['single'], diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts index 0bd97dffb34f8..bf10fb57f1a5c 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts @@ -297,7 +297,6 @@ describe('useApi', () => { await waitForNextUpdate(); await result.current.getExceptionListsItems({ - filterOptions: [], lists: [ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, ], @@ -313,7 +312,6 @@ describe('useApi', () => { }); const expected: ApiCallByListIdProps = { - filterOptions: [], http: mockKibanaHttpService, listIds: ['list_id'], namespaceTypes: ['single'], @@ -351,7 +349,6 @@ describe('useApi', () => { await waitForNextUpdate(); await result.current.getExceptionListsItems({ - filterOptions: [], lists: [ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, ], @@ -389,7 +386,6 @@ describe('useApi', () => { await waitForNextUpdate(); await result.current.getExceptionListsItems({ - filterOptions: [], lists: [ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, ], diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.test.ts deleted file mode 100644 index d8ae72d4d6205..0000000000000 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.test.ts +++ /dev/null @@ -1,498 +0,0 @@ -/* - * 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 { act, renderHook } from '@testing-library/react-hooks'; -import type { - ExceptionListItemSchema, - UseExceptionListItemsSuccess, - UseExceptionListProps, -} from '@kbn/securitysolution-io-ts-list-types'; -import * as api from '@kbn/securitysolution-list-api'; -import { - ReturnExceptionListAndItems, - transformInput, - useExceptionListItems, -} from '@kbn/securitysolution-list-hooks'; -import { coreMock } from '@kbn/core/public/mocks'; - -import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock'; - -jest.mock('uuid', () => ({ - v4: jest.fn().mockReturnValue('123'), -})); -jest.mock('@kbn/securitysolution-list-api'); - -const mockKibanaHttpService = coreMock.createStart().http; - -// TODO: Port all of this test code over to the package of: packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.test.ts once the mocks and kibana core mocks are figured out - -describe('useExceptionListItems', () => { - const onErrorMock = jest.fn(); - - beforeEach(() => { - jest - .spyOn(api, 'fetchExceptionListsItemsByListIds') - .mockResolvedValue(getFoundExceptionListItemSchemaMock()); - }); - - afterEach(() => { - onErrorMock.mockClear(); - jest.clearAllMocks(); - }); - - test('initializes hook', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseExceptionListProps, - ReturnExceptionListAndItems - >(() => - useExceptionListItems({ - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - ], - matchFilters: false, - onError: onErrorMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: false, - }) - ); - await waitForNextUpdate(); - - expect(result.current).toEqual([ - true, - [], - { - page: 1, - perPage: 20, - total: 0, - }, - null, - ]); - }); - }); - - test('fetches exception items', async () => { - await act(async () => { - const onSuccessMock = jest.fn(); - const { result, waitForNextUpdate } = renderHook< - UseExceptionListProps, - ReturnExceptionListAndItems - >(() => - useExceptionListItems({ - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - ], - matchFilters: false, - onError: onErrorMock, - onSuccess: onSuccessMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: false, - }) - ); - // NOTE: First `waitForNextUpdate` is initialization - // Second call applies the params - await waitForNextUpdate(); - await waitForNextUpdate(); - - const expectedListItemsResult: ExceptionListItemSchema[] = - getFoundExceptionListItemSchemaMock().data.map((item) => transformInput(item)); - const expectedResult: UseExceptionListItemsSuccess = { - exceptions: expectedListItemsResult, - pagination: { page: 1, perPage: 1, total: 1 }, - }; - - expect(result.current).toEqual([ - false, - expectedListItemsResult, - { - page: 1, - perPage: 1, - total: 1, - }, - result.current[3], - ]); - expect(onSuccessMock).toHaveBeenCalledWith(expectedResult); - }); - }); - - test('fetches only detection list items if "showDetectionsListsOnly" is true', async () => { - const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( - api, - 'fetchExceptionListsItemsByListIds' - ); - - await act(async () => { - const onSuccessMock = jest.fn(); - const { waitForNextUpdate } = renderHook( - () => - useExceptionListItems({ - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - { - id: 'myListIdEndpoint', - listId: 'list_id_endpoint', - namespaceType: 'agnostic', - type: 'endpoint', - }, - ], - matchFilters: false, - onError: onErrorMock, - onSuccess: onSuccessMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: true, - showEndpointListsOnly: false, - }) - ); - // NOTE: First `waitForNextUpdate` is initialization - // Second call applies the params - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledWith({ - filterOptions: [], - http: mockKibanaHttpService, - listIds: ['list_id'], - namespaceTypes: ['single'], - pagination: { page: 1, perPage: 20 }, - signal: new AbortController().signal, - }); - }); - }); - - test('fetches only detection list items if "showEndpointListsOnly" is true', async () => { - const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( - api, - 'fetchExceptionListsItemsByListIds' - ); - - await act(async () => { - const onSuccessMock = jest.fn(); - const { waitForNextUpdate } = renderHook( - () => - useExceptionListItems({ - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - { - id: 'myListIdEndpoint', - listId: 'list_id_endpoint', - namespaceType: 'agnostic', - type: 'endpoint', - }, - ], - matchFilters: false, - onError: onErrorMock, - onSuccess: onSuccessMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: true, - }) - ); - // NOTE: First `waitForNextUpdate` is initialization - // Second call applies the params - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledWith({ - filterOptions: [], - http: mockKibanaHttpService, - listIds: ['list_id_endpoint'], - namespaceTypes: ['agnostic'], - pagination: { page: 1, perPage: 20 }, - signal: new AbortController().signal, - }); - }); - }); - - test('does not fetch items if no lists to fetch', async () => { - const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( - api, - 'fetchExceptionListsItemsByListIds' - ); - - await act(async () => { - const onSuccessMock = jest.fn(); - const { result, waitForNextUpdate } = renderHook< - UseExceptionListProps, - ReturnExceptionListAndItems - >(() => - useExceptionListItems({ - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - ], - matchFilters: false, - onError: onErrorMock, - onSuccess: onSuccessMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: true, - }) - ); - // NOTE: First `waitForNextUpdate` is initialization - // Second call applies the params - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(spyOnfetchExceptionListsItemsByListIds).not.toHaveBeenCalled(); - expect(result.current).toEqual([ - false, - [], - { - page: 0, - perPage: 20, - total: 0, - }, - result.current[3], - ]); - }); - }); - - test('applies first filterOptions filter to all lists if "matchFilters" is true', async () => { - const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( - api, - 'fetchExceptionListsItemsByListIds' - ); - - await act(async () => { - const onSuccessMock = jest.fn(); - const { waitForNextUpdate } = renderHook( - () => - useExceptionListItems({ - filterOptions: [{ filter: 'host.name', tags: [] }], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - { - id: 'myListIdEndpoint', - listId: 'list_id_endpoint', - namespaceType: 'agnostic', - type: 'endpoint', - }, - ], - matchFilters: true, - onError: onErrorMock, - onSuccess: onSuccessMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: false, - }) - ); - // NOTE: First `waitForNextUpdate` is initialization - // Second call applies the params - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledWith({ - filterOptions: [ - { filter: 'host.name', tags: [] }, - { filter: 'host.name', tags: [] }, - ], - http: mockKibanaHttpService, - listIds: ['list_id', 'list_id_endpoint'], - namespaceTypes: ['single', 'agnostic'], - pagination: { page: 1, perPage: 20 }, - signal: new AbortController().signal, - }); - }); - }); - - test('fetches a new exception list and its items', async () => { - const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( - api, - 'fetchExceptionListsItemsByListIds' - ); - const onSuccessMock = jest.fn(); - await act(async () => { - const { rerender, waitForNextUpdate } = renderHook< - UseExceptionListProps, - ReturnExceptionListAndItems - >( - ({ - filterOptions, - http, - lists, - matchFilters, - pagination, - onError, - onSuccess, - showDetectionsListsOnly, - showEndpointListsOnly, - }) => - useExceptionListItems({ - filterOptions, - http, - lists, - matchFilters, - onError, - onSuccess, - pagination, - showDetectionsListsOnly, - showEndpointListsOnly, - }), - { - initialProps: { - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - ], - matchFilters: false, - onError: onErrorMock, - onSuccess: onSuccessMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: false, - }, - } - ); - // NOTE: First `waitForNextUpdate` is initialization - // Second call applies the params - await waitForNextUpdate(); - await waitForNextUpdate(); - - rerender({ - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'newListId', listId: 'new_list_id', namespaceType: 'single', type: 'detection' }, - ], - matchFilters: false, - onError: onErrorMock, - onSuccess: onSuccessMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: false, - }); - // NOTE: Only need one call here because hook already initilaized - await waitForNextUpdate(); - - expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledTimes(2); - }); - }); - - test('fetches list and items when refreshExceptionList callback invoked', async () => { - const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( - api, - 'fetchExceptionListsItemsByListIds' - ); - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseExceptionListProps, - ReturnExceptionListAndItems - >(() => - useExceptionListItems({ - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - ], - matchFilters: false, - onError: onErrorMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: false, - }) - ); - // NOTE: First `waitForNextUpdate` is initialization - // Second call applies the params - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(typeof result.current[3]).toEqual('function'); - - if (result.current[3] != null) { - result.current[3](); - } - // NOTE: Only need one call here because hook already initilaized - await waitForNextUpdate(); - - expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledTimes(2); - }); - }); - - test('invokes "onError" callback if "fetchExceptionListsItemsByListIds" fails', async () => { - const mockError = new Error('failed to fetches list items'); - const spyOnfetchExceptionListsItemsByListIds = jest - .spyOn(api, 'fetchExceptionListsItemsByListIds') - .mockRejectedValue(mockError); - await act(async () => { - const { waitForNextUpdate } = renderHook( - () => - useExceptionListItems({ - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - ], - matchFilters: false, - onError: onErrorMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: false, - }) - ); - // NOTE: First `waitForNextUpdate` is initialization - // Second call applies the params - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(onErrorMock).toHaveBeenCalledWith(mockError); - expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts index 7a58078dc4e6e..f3fd291ecd067 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts @@ -40,7 +40,7 @@ interface FindExceptionListItemsOptions { sortField: SortFieldOrUndefined; sortOrder: SortOrderOrUndefined; searchAfter: SearchAfterOrUndefined; - search?: SearchOrUndefined; + search: SearchOrUndefined; } export const findExceptionListsItem = async ({ diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx deleted file mode 100644 index 4d38fac340727..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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 { mount } from 'enzyme'; - -import { ExceptionsViewerPagination } from './exceptions_pagination'; - -describe('ExceptionsViewerPagination', () => { - it('it renders passed in "pageSize" as selected option', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="exceptionsPerPageBtn"]').at(0).text()).toEqual( - 'Items per page: 50' - ); - }); - - it('it renders all passed in page size options when per page button clicked', () => { - const wrapper = mount( - - ); - - wrapper.find('[data-test-subj="exceptionsPerPageBtn"] button').simulate('click'); - - expect(wrapper.find('button[data-test-subj="exceptionsPerPageItem"]').at(0).text()).toEqual( - '20 items' - ); - expect(wrapper.find('button[data-test-subj="exceptionsPerPageItem"]').at(1).text()).toEqual( - '50 items' - ); - expect(wrapper.find('button[data-test-subj="exceptionsPerPageItem"]').at(2).text()).toEqual( - '100 items' - ); - }); - - it('it invokes "onPaginationChange" when per page item is clicked', () => { - const mockOnPaginationChange = jest.fn(); - const wrapper = mount( - - ); - - wrapper.find('[data-test-subj="exceptionsPerPageBtn"] button').simulate('click'); - wrapper.find('button[data-test-subj="exceptionsPerPageItem"]').at(0).simulate('click'); - - expect(mockOnPaginationChange).toHaveBeenCalledWith({ - pagination: { pageIndex: 0, pageSize: 20, totalItemCount: 1 }, - }); - }); - - it('it renders correct total page count', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="exceptionsPagination"]').at(0).prop('pageCount')).toEqual( - 4 - ); - expect( - wrapper.find('[data-test-subj="exceptionsPagination"]').at(0).prop('activePage') - ).toEqual(0); - }); - - it('it invokes "onPaginationChange" when next clicked', () => { - const mockOnPaginationChange = jest.fn(); - const wrapper = mount( - - ); - - wrapper.find('[data-test-subj="pagination-button-next"]').at(1).simulate('click'); - - expect(mockOnPaginationChange).toHaveBeenCalledWith({ - pagination: { pageIndex: 1, pageSize: 50, totalItemCount: 160 }, - }); - }); - - it('it invokes "onPaginationChange" when page clicked', () => { - const mockOnPaginationChange = jest.fn(); - const wrapper = mount( - - ); - - wrapper.find('button[data-test-subj="pagination-button-3"]').simulate('click'); - - expect(mockOnPaginationChange).toHaveBeenCalledWith({ - pagination: { pageIndex: 3, pageSize: 50, totalItemCount: 160 }, - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx deleted file mode 100644 index 41cccc1cf8ba8..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import styled from 'styled-components'; - -import type { - ExceptionListItemSchema, - ExceptionListTypeEnum, -} from '@kbn/securitysolution-io-ts-list-types'; - -import { ExceptionItemCard } from './exception_item_card'; -import type { ExceptionListItemIdentifiers } from '../types'; -import type { RuleReferences } from '../use_find_references'; - -const MyFlexItem = styled(EuiFlexItem)` - margin: ${({ theme }) => `${theme.eui.euiSize} 0`}; - &:first-child { - margin: ${({ theme }) => `${theme.eui.euiSizeXS} 0 ${theme.eui.euiSize}`}; - } -`; - -interface ExceptionItemsViewerProps { - disableActions: boolean; - exceptions: ExceptionListItemSchema[]; - loadingItemIds: ExceptionListItemIdentifiers[]; - listType: ExceptionListTypeEnum; - ruleReferences: RuleReferences | null; - onDeleteException: (arg: ExceptionListItemIdentifiers) => void; - onEditExceptionItem: (item: ExceptionListItemSchema) => void; -} - -const ExceptionItemsViewerComponent: React.FC = ({ - exceptions, - loadingItemIds, - listType, - disableActions, - ruleReferences, - onDeleteException, - onEditExceptionItem, -}): JSX.Element => { - return ( - - - - {exceptions.map((exception) => ( - - - - ))} - - - - ); -}; - -ExceptionItemsViewerComponent.displayName = 'ExceptionItemsViewerComponent'; - -export const ExceptionsViewerItems = React.memo(ExceptionItemsViewerComponent); - -ExceptionsViewerItems.displayName = 'ExceptionsViewerItems'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/no_exception_items.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/no_exception_items.tsx deleted file mode 100644 index 84f16e73324ae..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/no_exception_items.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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, { useCallback } from 'react'; -import { EuiFlexItem, EuiEmptyPrompt, EuiButton, useEuiTheme } from '@elastic/eui'; - -import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import * as i18n from './translations'; - -interface ExeptionItemsViewerNoItemsComponentProps { - listType: ExceptionListTypeEnum; - onCreateExceptionListItem: () => void; -} - -const ExeptionItemsViewerNoItemsComponent = ({ - listType, - onCreateExceptionListItem, -}: ExeptionItemsViewerNoItemsComponentProps): JSX.Element => { - const { euiTheme } = useEuiTheme(); - const handleAddException = useCallback(() => { - onCreateExceptionListItem(); - }, [onCreateExceptionListItem]); - - return ( - - {i18n.EXCEPTION_EMPTY_PROMPT_TITLE} - } - body={ -

- {listType === ExceptionListTypeEnum.ENDPOINT - ? i18n.EXCEPTION_EMPTY_ENDPOINT_PROMPT_BODY - : i18n.EXCEPTION_EMPTY_PROMPT_BODY} -

- } - actions={ - - {listType === ExceptionListTypeEnum.ENDPOINT - ? i18n.EXCEPTION_EMPTY_PROMPT_ENDPOINT_BUTTON - : i18n.EXCEPTION_EMPTY_PROMPT_BUTTON} - - } - data-test-subj="exceptionsEmptyPrompt" - /> -
- ); -}; - -ExeptionItemsViewerNoItemsComponent.displayName = 'ExeptionItemsViewerNoItemsComponent'; - -export const ExeptionItemsViewerNoItems = React.memo(ExeptionItemsViewerNoItemsComponent); - -ExeptionItemsViewerNoItems.displayName = 'ExeptionItemsViewerNoItems'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/no_search_results.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/no_search_results.tsx deleted file mode 100644 index 448bbcd30b23f..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/no_search_results.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 { EuiFlexItem, EuiEmptyPrompt } from '@elastic/eui'; - -import * as i18n from './translations'; - -const ExeptionItemsViewerEmptySearchResultsComponent = (): JSX.Element => ( - - - {i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY} - - } - data-test-subj="exceptionItemsNoSearchResultsPrompt" - /> - -); - -ExeptionItemsViewerEmptySearchResultsComponent.displayName = - 'ExeptionItemsViewerEmptySearchResultsComponent'; - -export const ExeptionItemsViewerEmptySearchResults = React.memo( - ExeptionItemsViewerEmptySearchResultsComponent -); - -ExeptionItemsViewerEmptySearchResults.displayName = 'ExeptionItemsViewerEmptySearchResults'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/use_initial_exceptions_viewer_render.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/use_initial_exceptions_viewer_render.tsx deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx index 5945b1a0111c0..de5eca78aaffb 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx @@ -14,30 +14,30 @@ import { AddExceptionFlyout } from '.'; import { getExceptionBuilderComponentLazy } from '@kbn/lists-plugin/public'; import { useAsync } from '@kbn/securitysolution-hook-utils'; import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock'; -import { useFetchIndex } from '../../../containers/source'; +import { useFetchIndex } from '../../../../common/containers/source'; import { createStubIndexPattern, stubIndexPattern } from '@kbn/data-plugin/common/stubs'; -import { useAddOrUpdateException } from '../use_add_exception'; -import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; +import { useAddOrUpdateException } from '../../logic/use_add_exception'; +import { useFetchOrCreateRuleExceptionList } from '../../logic/use_fetch_or_create_rule_exception_list'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; -import * as helpers from '../helpers'; +import * as helpers from '../../utils/helpers'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; -import { TestProviders } from '../../../mock'; +import { TestProviders } from '../../../../common/mock'; import { getRulesEqlSchemaMock, getRulesSchemaMock, } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; -import type { AlertData } from '../types'; +import type { AlertData } from '../../utils/types'; jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index'); -jest.mock('../../../lib/kibana'); -jest.mock('../../../containers/source'); +jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/containers/source'); jest.mock('../../../../detections/containers/detection_engine/rules'); -jest.mock('../use_add_exception'); -jest.mock('../use_fetch_or_create_rule_exception_list'); +jest.mock('../../logic/use_add_exception'); +jest.mock('../../logic/use_fetch_or_create_rule_exception_list'); jest.mock('@kbn/securitysolution-hook-utils', () => ({ ...jest.requireActual('@kbn/securitysolution-hook-utils'), useAsync: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx index e5ae187d79d34..1a547b6e62d60 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx @@ -46,17 +46,17 @@ import { isThresholdRule, } from '../../../../../common/detection_engine/utils'; import type { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; -import * as i18nCommon from '../../../translations'; +import * as i18nCommon from '../../../../common/translations'; import * as i18n from './translations'; -import * as sharedI18n from '../translations'; -import { useAppToasts } from '../../../hooks/use_app_toasts'; -import { useKibana } from '../../../lib/kibana'; -import { Loader } from '../../loader'; -import { useAddOrUpdateException } from '../use_add_exception'; +import * as sharedI18n from '../../utils/translations'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useKibana } from '../../../../common/lib/kibana'; +import { Loader } from '../../../../common/components/loader'; +import { useAddOrUpdateException } from '../../logic/use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; -import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; -import { AddExceptionComments } from '../add_exception_comments'; +import { useFetchOrCreateRuleExceptionList } from '../../logic/use_fetch_or_create_rule_exception_list'; +import { ExceptionItemComments } from '../item_comments'; import { enrichNewExceptionItemsWithComments, enrichExceptionItemsWithOS, @@ -66,11 +66,11 @@ import { entryHasNonEcsType, retrieveAlertOsTypes, filterIndexPatterns, -} from '../helpers'; +} from '../../utils/helpers'; import type { ErrorInfo } from '../error_callout'; import { ErrorCallout } from '../error_callout'; -import type { AlertData } from '../types'; -import { useFetchIndex } from '../../../containers/source'; +import type { AlertData } from '../../utils/types'; +import { useFetchIndex } from '../../../../common/containers/source'; export interface AddExceptionFlyoutProps { ruleName: string; @@ -549,7 +549,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({ - diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/translations.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/translations.ts diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.test.tsx similarity index 57% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.test.tsx index aae467ca921a7..f71a07d4abc60 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.test.tsx @@ -10,11 +10,12 @@ import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; -import { ExceptionsViewerItems } from './exceptions_viewer_items'; -import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; -import { TestProviders } from '../../../mock'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import { ExceptionsViewerItems } from './all_items'; +import { getMockTheme } from '../../../../common/lib/kibana/kibana_react.mock'; +import { TestProviders } from '../../../../common/mock'; + const mockTheme = getMockTheme({ eui: { euiSize: '10px', @@ -24,18 +25,16 @@ const mockTheme = getMockTheme({ }); describe('ExceptionsViewerItems', () => { - it('it renders empty prompt if "showEmpty" is "true"', () => { + it('it renders empty prompt if "viewerState" is "empty"', () => { const wrapper = mount( { ); - expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-empty-detection"]').exists() + ).toBeTruthy(); expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); }); - it('it renders no search results found prompt if "showNoResults" is "true"', () => { + it('it renders no search results found prompt if "viewerState" is "empty_search"', () => { const wrapper = mount( { ); expect( - wrapper.find('[data-test-subj="exceptionItemsNoSearchResultsPrompt"]').exists() + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-emptySearch"]').exists() ).toBeTruthy(); expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); }); - it('it renders exceptions if "showEmpty" and "isInitLoading" is "false", and exceptions exist', () => { + it('it renders exceptions if "viewerState" and "null"', () => { const wrapper = mount( { ); expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeFalsy(); - }); - - it('it does not render exceptions if "isInitLoading" is "true"', () => { - const wrapper = mount( - - - - - - ); - - expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="exceptionsLoadingPrompt"]').exists()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.tsx new file mode 100644 index 0000000000000..63b5276952947 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.tsx @@ -0,0 +1,91 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +import type { + ExceptionListItemSchema, + ExceptionListTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; + +import { ExceptionItemCard } from '../exception_item_card'; +import type { ExceptionListItemIdentifiers } from '../../utils/types'; +import type { RuleReferences } from '../../logic/use_find_references'; +import type { ViewerState } from './reducer'; +import { ExeptionItemsViewerEmptyPrompts } from './empty_viewer_state'; + +const MyFlexItem = styled(EuiFlexItem)` + margin: ${({ theme }) => `${theme.eui.euiSize} 0`}; + &:first-child { + margin: ${({ theme }) => `${theme.eui.euiSizeXS} 0 ${theme.eui.euiSize}`}; + } +`; + +interface ExceptionItemsViewerProps { + disableActions: boolean; + exceptions: ExceptionListItemSchema[]; + loadingItemIds: ExceptionListItemIdentifiers[]; + listType: ExceptionListTypeEnum; + ruleReferences: RuleReferences | null; + viewerState: ViewerState; + onCreateExceptionListItem: () => void; + onDeleteException: (arg: ExceptionListItemIdentifiers) => void; + onEditExceptionItem: (item: ExceptionListItemSchema) => void; +} + +const ExceptionItemsViewerComponent: React.FC = ({ + exceptions, + loadingItemIds, + listType, + disableActions, + ruleReferences, + viewerState, + onCreateExceptionListItem, + onDeleteException, + onEditExceptionItem, +}): JSX.Element => { + return ( + <> + {viewerState != null ? ( + + ) : ( + + + + {exceptions.map((exception) => ( + + + + ))} + + + + )} + + ); +}; + +ExceptionItemsViewerComponent.displayName = 'ExceptionItemsViewerComponent'; + +export const ExceptionsViewerItems = React.memo(ExceptionItemsViewerComponent); + +ExceptionsViewerItems.displayName = 'ExceptionsViewerItems'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.test.tsx new file mode 100644 index 0000000000000..8ff59c563bd90 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.test.tsx @@ -0,0 +1,84 @@ +/* + * 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 { mount } from 'enzyme'; + +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + +import { ExeptionItemsViewerEmptyPrompts } from './empty_viewer_state'; +import * as i18n from './translations'; + +describe('ExeptionItemsViewerEmptyPrompts', () => { + it('it renders loading screen when "currentState" is "loading"', () => { + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-loading"]').exists() + ).toBeTruthy(); + }); + + it('it renders empty search screen when "currentState" is "empty_search"', () => { + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-emptySearch"]').exists() + ).toBeTruthy(); + }); + + it('it renders no endpoint items screen when "currentState" is "empty" and "listType" is "endpoint"', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptBody"]').at(0).text()).toEqual( + i18n.EXCEPTION_EMPTY_ENDPOINT_PROMPT_BODY + ); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptButton"]').at(0).text()).toEqual( + i18n.EXCEPTION_EMPTY_PROMPT_ENDPOINT_BUTTON + ); + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-empty-endpoint"]').exists() + ).toBeTruthy(); + }); + + it('it renders no exception items screen when "currentState" is "empty" and "listType" is "detection"', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptBody"]').at(0).text()).toEqual( + i18n.EXCEPTION_EMPTY_PROMPT_BODY + ); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptButton"]').at(0).text()).toEqual( + i18n.EXCEPTION_EMPTY_PROMPT_BUTTON + ); + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-empty-detection"]').exists() + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/empty_viewer_state.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.tsx similarity index 79% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/empty_viewer_state.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.tsx index 5af7998135144..c26b8cbc60d72 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/empty_viewer_state.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.tsx @@ -6,15 +6,22 @@ */ import React from 'react'; -import { EuiEmptyPrompt, EuiEmptyPromptProps } from '@elastic/eui'; -import { EuiButton, useEuiTheme, EuiPageTemplate, EuiLoadingLogo } from '@elastic/eui'; +import type { EuiEmptyPromptProps } from '@elastic/eui'; +import { + EuiEmptyPrompt, + EuiButton, + useEuiTheme, + EuiPageTemplate, + EuiLoadingLogo, +} from '@elastic/eui'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import * as i18n from './translations'; +import type { ViewerState } from './reducer'; -interface ExeptionItemsViewerNoItemsComponentProps { +interface ExeptionItemsViewerEmptyPromptsComponentProps { listType: ExceptionListTypeEnum; - currentState: string; + currentState: ViewerState; onCreateExceptionListItem: () => void; } @@ -22,7 +29,7 @@ const ExeptionItemsViewerEmptyPromptsComponent = ({ listType, currentState, onCreateExceptionListItem, -}: ExeptionItemsViewerNoItemsComponentProps): JSX.Element => { +}: ExeptionItemsViewerEmptyPromptsComponentProps): JSX.Element => { const { euiTheme } = useEuiTheme(); let emptyPromptProps: Partial; @@ -33,6 +40,7 @@ const ExeptionItemsViewerEmptyPromptsComponent = ({ iconType: 'alert', title:

{i18n.EXCEPTION_ERROR_TITLE}

, body:

{i18n.EXCEPTION_ERROR_DESCRIPTION}

, + 'data-test-subj': 'exceptionItemViewerEmptyPrompts-error', }; break; case 'empty': @@ -52,6 +60,7 @@ const ExeptionItemsViewerEmptyPromptsComponent = ({ ), actions: [ , ], + 'data-test-subj': `exceptionItemViewerEmptyPrompts-empty-${listType}`, }; break; case 'empty_search': @@ -77,6 +87,7 @@ const ExeptionItemsViewerEmptyPromptsComponent = ({ {i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY}

), + 'data-test-subj': 'exceptionItemViewerEmptyPrompts-emptySearch', }; break; default: @@ -84,6 +95,7 @@ const ExeptionItemsViewerEmptyPromptsComponent = ({ color: 'subdued', icon: , title:

{i18n.EXCEPTION_LOADING_TITLE}

, + 'data-test-subj': 'exceptionItemViewerEmptyPrompts-loading', }; break; } diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.test.tsx similarity index 53% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.test.tsx index 2d21c5fef200c..c92779e248aa2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.test.tsx @@ -8,21 +8,18 @@ import React from 'react'; import { mount } from 'enzyme'; -import { ExceptionsViewer } from '.'; -import { useKibana } from '../../../lib/kibana'; -import { useExceptionListItems, useApi } from '@kbn/securitysolution-list-hooks'; - import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock'; -import { getFoundExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/found_exception_list_item_schema.mock'; -import { TestProviders } from '../../../mock'; + +import { ExceptionsViewer } from '.'; +import { useKibana } from '../../../../common/lib/kibana'; +import { TestProviders } from '../../../../common/mock'; import type { Rule } from '../../../../detections/containers/detection_engine/rules/types'; import { mockRule } from '../../../../detections/pages/detection_engine/rules/all/__mocks__/mock'; -import { useFindExceptionListReferences } from '../use_find_references'; +import { useFindExceptionListReferences } from '../../logic/use_find_references'; -jest.mock('../../../lib/kibana'); +jest.mock('../../../../common/lib/kibana'); jest.mock('@kbn/securitysolution-list-hooks'); -jest.mock('../use_find_references'); +jest.mock('../../logic/use_find_references'); const getMockRule = (): Rule => ({ ...mockRule('123'), @@ -47,50 +44,24 @@ describe('ExceptionsViewer', () => { }, }); - (useApi as jest.Mock).mockReturnValue({ - deleteExceptionItem: jest.fn().mockResolvedValue(true), - getExceptionListsItems: jest.fn().mockResolvedValue(getFoundExceptionListItemSchemaMock()), - }); - - (useExceptionListItems as jest.Mock).mockReturnValue([ - false, - [], - [], - { - page: 1, - perPage: 20, - total: 0, - }, - jest.fn(), - ]); - (useFindExceptionListReferences as jest.Mock).mockReturnValue([false, null]); }); it('it renders loader if "loadingList" is true', () => { - (useExceptionListItems as jest.Mock).mockReturnValue([ - true, - [], - [], - { - page: 1, - perPage: 20, - total: 0, - }, - jest.fn(), - ]); const wrapper = mount( @@ -103,8 +74,17 @@ describe('ExceptionsViewer', () => { const wrapper = mount( @@ -114,30 +94,20 @@ describe('ExceptionsViewer', () => { }); it('it renders empty prompt if no exception items exist', () => { - (useExceptionListItems as jest.Mock).mockReturnValue([ - false, - [getExceptionListSchemaMock()], - [], - { - page: 1, - perPage: 20, - total: 0, - }, - jest.fn(), - ]); - const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx similarity index 87% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx index 2c3525ed74472..75f365aeb2c33 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx @@ -22,21 +22,22 @@ import { } from '@kbn/securitysolution-list-api'; import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; import { useUserData } from '../../../../detections/components/user_info'; -import { useKibana, useToasts } from '../../../lib/kibana'; -import { ExceptionsViewerHeader } from './exceptions_viewer_header'; -import type { ExceptionListItemIdentifiers } from '../types'; +import { useKibana, useToasts } from '../../../../common/lib/kibana'; +import { ExceptionsViewerSearchBar } from './search_bar'; +import type { ExceptionListItemIdentifiers } from '../../utils/types'; import type { State, ViewerFlyoutName, ViewerState } from './reducer'; import { allExceptionItemsReducer } from './reducer'; -import { ExceptionsViewerPagination } from './exceptions_pagination'; -import { ExceptionsViewerUtility } from './exceptions_utility'; -import { ExceptionsViewerItems } from './exceptions_viewer_items'; +import { ExceptionsViewerPagination } from './pagination'; +import { ExceptionsViewerUtility } from './utility_bar'; +import { ExceptionsViewerItems } from './all_items'; import { EditExceptionFlyout } from '../edit_exception_flyout'; import { AddExceptionFlyout } from '../add_exception_flyout'; import * as i18n from './translations'; -import { useFindExceptionListReferences } from '../use_find_references'; +import { useFindExceptionListReferences } from '../../logic/use_find_references'; import type { Rule } from '../../../../detections/containers/detection_engine/rules/types'; -import { ExeptionItemsViewerEmptyPrompts } from './empty_viewer_state'; + +const STATES_SEARCH_HIDDEN: ViewerState[] = ['error', 'empty', 'loading']; const initialState: State = { pagination: { @@ -150,7 +151,7 @@ const ExceptionsViewerComponent = ({ const newPagination = options?.pagination != null ? { - page: options.pagination.page + 1, + page: (options.pagination.page ?? 0) + 1, perPage: options.pagination.perPage, } : { @@ -164,7 +165,7 @@ const ExceptionsViewerComponent = ({ total, data, } = await fetchExceptionListsItemsByListIds({ - filters: undefined, + filter: undefined, http: services.http, listIds: exceptionLists.map((list) => list.list_id), namespaceTypes: exceptionLists.map((list) => list.namespace_type), @@ -362,41 +363,41 @@ const ExceptionsViewerComponent = ({ )} - {viewerState != null ? ( - + {!STATES_SEARCH_HIDDEN.includes(viewerState) && ( + <> + + + + + + + )} + + - ) : ( - <> - - - - - - - + {!STATES_SEARCH_HIDDEN.includes(viewerState) && ( - - )} + )} + ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/pagination.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/pagination.test.tsx new file mode 100644 index 0000000000000..5bef650a4dc77 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/pagination.test.tsx @@ -0,0 +1,71 @@ +/* + * 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 { mount } from 'enzyme'; + +import { ExceptionsViewerPagination } from './pagination'; + +describe('ExceptionsViewerPagination', () => { + it('it invokes "onPaginationChange" when per page item is clicked', () => { + const mockOnPaginationChange = jest.fn(); + const wrapper = mount( + + ); + + wrapper.find('button[data-test-subj="tablePaginationPopoverButton"]').at(0).simulate('click'); + wrapper.find('button[data-test-subj="tablePagination-50-rows"]').at(0).simulate('click'); + + expect(mockOnPaginationChange).toHaveBeenCalledWith({ page: 0, perPage: 50 }); + }); + + it('it invokes "onPaginationChange" when next clicked', () => { + const mockOnPaginationChange = jest.fn(); + const wrapper = mount( + + ); + + wrapper.find('button[data-test-subj="pagination-button-next"]').at(0).simulate('click'); + + expect(mockOnPaginationChange).toHaveBeenCalledWith({ page: 1, perPage: 5 }); + }); + + it('it invokes "onPaginationChange" when page clicked', () => { + const mockOnPaginationChange = jest.fn(); + const wrapper = mount( + + ); + + wrapper.find('button[data-test-subj="pagination-button-2"]').simulate('click'); + + expect(mockOnPaginationChange).toHaveBeenCalledWith({ page: 2, perPage: 50 }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/pagination.tsx similarity index 88% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/pagination.tsx index f53b7c4536150..5ef25303f1da0 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/pagination.tsx @@ -8,7 +8,8 @@ import React, { useCallback } from 'react'; import { EuiTablePagination } from '@elastic/eui'; -import type { ExceptionsPagination } from '../types'; +import type { ExceptionsPagination } from '../../utils/types'; +import * as i18n from './translations'; interface ExceptionsViewerPaginationProps { pagination: ExceptionsPagination; @@ -41,13 +42,14 @@ const ExceptionsViewerPaginationComponent = ({ return ( ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/reducer.ts similarity index 98% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/reducer.ts index 91691dce9ab5e..fd3913de214fa 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/reducer.ts @@ -8,11 +8,10 @@ import type { ExceptionListType, ExceptionListItemSchema, - ExceptionListIdentifiers, Pagination, ListArray, } from '@kbn/securitysolution-io-ts-list-types'; -import type { ExceptionsPagination, ExceptionListItemIdentifiers } from '../types'; +import type { ExceptionsPagination, ExceptionListItemIdentifiers } from '../../utils/types'; export type ViewerFlyoutName = 'addException' | 'editException' | null; export type ViewerState = 'error' | 'empty' | 'empty_search' | 'loading' | 'searching' | null; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.test.tsx similarity index 55% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.test.tsx index ff5c9ff758604..903e3731f6fc0 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.test.tsx @@ -8,39 +8,16 @@ import React from 'react'; import { mount } from 'enzyme'; -import { ExceptionsViewerHeader } from './exceptions_viewer_header'; - import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -describe('ExceptionsViewerHeader', () => { - it('it renders all disabled if "isInitLoading" is true', () => { - const wrapper = mount( - - ); - - expect( - wrapper.find('input[data-test-subj="exceptionsHeaderSearch"]').at(0).prop('disabled') - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button') - .at(0) - .prop('disabled') - ).toBeTruthy(); - }); +import { ExceptionsViewerSearchBar } from './search_bar'; +describe('ExceptionsViewerSearchBar', () => { it('it does not display add exception button if user is read only', () => { const wrapper = mount( - @@ -52,11 +29,10 @@ describe('ExceptionsViewerHeader', () => { it('it invokes "onAddExceptionClick" when user selects to add an exception item', () => { const mockOnAddExceptionClick = jest.fn(); const wrapper = mount( - ); @@ -72,11 +48,10 @@ describe('ExceptionsViewerHeader', () => { it('it invokes "onAddExceptionClick" when user selects to add an endpoint exception item', () => { const mockOnAddExceptionClick = jest.fn(); const wrapper = mount( - ); @@ -88,25 +63,4 @@ describe('ExceptionsViewerHeader', () => { ); expect(mockOnAddExceptionClick).toHaveBeenCalledWith('endpoint'); }); - - it('it invokes "onFilterChange" when search used and "Enter" pressed', () => { - const mockOnFilterChange = jest.fn(); - const wrapper = mount( - - ); - - wrapper.find('EuiFieldSearch').at(0).simulate('keyup', { - charCode: 13, - code: 'Enter', - key: 'Enter', - }); - - expect(mockOnFilterChange).toHaveBeenCalled(); - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx similarity index 84% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx index b51ff0f4e81f9..03ca172202719 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiSearchBar } from '@elastic/eui'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import * as i18n from '../translations'; +import * as i18n from '../../utils/translations'; const ITEMS_SCHEMA = { strict: true, @@ -41,7 +41,7 @@ const ITEMS_SCHEMA = { }, }; -interface ExceptionsViewerHeaderProps { +interface ExceptionsViewerSearchBarProps { isReadOnly: boolean; listType: ExceptionListTypeEnum; onSearch: (arg: string) => void; @@ -51,12 +51,12 @@ interface ExceptionsViewerHeaderProps { /** * Search exception items and take actions (to creat an item) */ -const ExceptionsViewerHeaderComponent = ({ +const ExceptionsViewerSearchBarComponent = ({ isReadOnly, listType, onSearch, onAddExceptionClick, -}: ExceptionsViewerHeaderProps): JSX.Element => { +}: ExceptionsViewerSearchBarProps): JSX.Element => { const handleOnSearch = useCallback( ({ queryText }): void => { onSearch(queryText); @@ -102,8 +102,8 @@ const ExceptionsViewerHeaderComponent = ({ ); }; -ExceptionsViewerHeaderComponent.displayName = 'ExceptionsViewerHeaderComponent'; +ExceptionsViewerSearchBarComponent.displayName = 'ExceptionsViewerSearchBarComponent'; -export const ExceptionsViewerHeader = React.memo(ExceptionsViewerHeaderComponent); +export const ExceptionsViewerSearchBar = React.memo(ExceptionsViewerSearchBarComponent); -ExceptionsViewerHeader.displayName = 'ExceptionsViewerHeader'; +ExceptionsViewerSearchBar.displayName = 'ExceptionsViewerSearchBar'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts similarity index 57% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/translations.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts index 687945bfb1c16..d2ea6a648c9a4 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts @@ -8,35 +8,35 @@ import { i18n } from '@kbn/i18n'; export const EXCEPTION_NO_SEARCH_RESULTS_PROMPT_TITLE = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.noSearchResultsPromptTitle', + 'xpack.securitySolution.exceptions.allItems.noSearchResultsPromptTitle', { defaultMessage: 'No results', } ); export const EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody', + 'xpack.securitySolution.exceptions.allItems.noSearchResultsPromptBody', { defaultMessage: 'No matching exception items were found in your search.', } ); export const EXCEPTION_EMPTY_PROMPT_TITLE = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.addExceptionsEmptyPromptTitle', + 'xpack.securitySolution.exceptions.allItems.addExceptionsEmptyPromptTitle', { defaultMessage: 'Add exceptions to this rule', } ); export const EXCEPTION_EMPTY_PROMPT_BODY = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.emptyPromptBody', + 'xpack.securitySolution.exceptions.allItems.emptyPromptBody', { defaultMessage: 'There are no exceptions on your rule. Create your first rule exception.', } ); export const EXCEPTION_EMPTY_ENDPOINT_PROMPT_BODY = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.endpoint.emptyPromptBody', + 'xpack.securitySolution.exceptions.allItems.endpoint.emptyPromptBody', { defaultMessage: 'There are no endpoint exceptions. Endpoint exceptions are applied to the endpoint and the detection rule. Create your first endpoint exception.', @@ -44,50 +44,65 @@ export const EXCEPTION_EMPTY_ENDPOINT_PROMPT_BODY = i18n.translate( ); export const EXCEPTION_EMPTY_PROMPT_BUTTON = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.emptyPromptButtonLabel', + 'xpack.securitySolution.exceptions.allItems.emptyPromptButtonLabel', { defaultMessage: 'Add rule exception', } ); export const EXCEPTION_EMPTY_PROMPT_ENDPOINT_BUTTON = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.endpoint.emptyPromptButtonLabel', + 'xpack.securitySolution.exceptions.allItems.endpoint.emptyPromptButtonLabel', { defaultMessage: 'Add endpoint exception', } ); export const EXCEPTION_LOADING_TITLE = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.loadingExceptionsTitle', + 'xpack.securitySolution.exceptions.allItems.loadingExceptionsTitle', { defaultMessage: 'Loading exceptions', } ); export const EXCEPTION_ERROR_TITLE = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.exceptionItemsFetchError', + 'xpack.securitySolution.exceptions.allItems.exceptionItemsFetchError', { defaultMessage: 'Unable to load exception items', } ); export const EXCEPTION_ERROR_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.exceptionItemsFetchErrorDescription', + 'xpack.securitySolution.exceptions.allItems.exceptionItemsFetchErrorDescription', { - defaultMessage: 'There was an error loading the exception items. Contact your administrator for help.', + defaultMessage: + 'There was an error loading the exception items. Contact your administrator for help.', } ); export const EXCEPTION_SEARCH_ERROR_TITLE = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.exceptionItemSearchErrorTitle', + 'xpack.securitySolution.exceptions.allItems.exceptionItemSearchErrorTitle', { defaultMessage: 'Error searching', } ); export const EXCEPTION_SEARCH_ERROR_BODY = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.exceptionItemSearchErrorBody', + 'xpack.securitySolution.exceptions.allItems.exceptionItemSearchErrorBody', { defaultMessage: 'An error occurred searching for exception items. Please try again.', } ); + +export const EXCEPTION_DELETE_ERROR_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.exceptionDeleteErrorTitle', + { + defaultMessage: 'Error deleting exception item', + } +); + +export const EXCEPTION_ITEMS_PAGINATION_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.paginationAriaLabel', + { + defaultMessage: 'Exception item table pagination', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.test.tsx similarity index 88% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.test.tsx index 49bb72764edb2..18b987aeb9a76 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.test.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { ExceptionsViewerUtility } from './exceptions_utility'; -import { TestProviders } from '../../../mock'; +import { ExceptionsViewerUtility } from './utility_bar'; +import { TestProviders } from '../../../common/mock'; describe('ExceptionsViewerUtility', () => { it('it renders correct item counts', () => { @@ -19,7 +19,7 @@ describe('ExceptionsViewerUtility', () => { pagination={{ pageIndex: 0, pageSize: 50, - totalItemCount: 2, + totalItemCount: 105, pageSizeOptions: [5, 10, 20, 50, 100], }} lastUpdated={1660534202} @@ -28,7 +28,7 @@ describe('ExceptionsViewerUtility', () => { ); expect(wrapper.find('[data-test-subj="exceptionsShowing"]').at(0).text()).toEqual( - 'Showing 1-2 of 2' + 'Showing 1-50 of 105' ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.tsx similarity index 82% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.tsx index a4b8b29bc460a..75b7782f171ea 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.tsx @@ -10,9 +10,14 @@ import { EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { ExceptionsPagination } from '../types'; -import { UtilityBar, UtilityBarSection, UtilityBarGroup, UtilityBarText } from '../../utility_bar'; -import { FormattedRelativePreferenceDate } from '../../formatted_date'; +import type { ExceptionsPagination } from '../../utils/types'; +import { + UtilityBar, + UtilityBarSection, + UtilityBarGroup, + UtilityBarText, +} from '../../../../common/components/utility_bar'; +import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; const StyledText = styled.span` font-weight: bold; @@ -46,7 +51,12 @@ const ExceptionsViewerUtilityComponent: React.FC = id="xpack.securitySolution.exceptions.viewer.paginationDetails" defaultMessage="Showing {partOne} of {partTwo}" values={{ - partOne: {`1-${pagination.totalItemCount}`}, + partOne: ( + {`1-${Math.min( + pagination.pageSize, + pagination.totalItemCount + )}`} + ), partTwo: {`${pagination.totalItemCount}`}, }} /> diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.test.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.test.tsx index 2885920222b5d..d5d5c3fc37316 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.test.tsx @@ -12,10 +12,10 @@ import type { ReactWrapper } from 'enzyme'; import { mount } from 'enzyme'; import { EditExceptionFlyout } from '.'; -import { useCurrentUser } from '../../../lib/kibana'; -import { useFetchIndex } from '../../../containers/source'; +import { useCurrentUser } from '../../../../common/lib/kibana'; +import { useFetchIndex } from '../../../../common/containers/source'; import { stubIndexPattern, createStubIndexPattern } from '@kbn/data-plugin/common/stubs'; -import { useAddOrUpdateException } from '../use_add_exception'; +import { useAddOrUpdateException } from '../../logic/use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; @@ -24,7 +24,7 @@ import { getRulesSchemaMock, } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; -import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; +import { getMockTheme } from '../../../../common/lib/kibana/kibana_react.mock'; import { getExceptionBuilderComponentLazy } from '@kbn/lists-plugin/public'; const mockTheme = getMockTheme({ @@ -36,11 +36,11 @@ const mockTheme = getMockTheme({ }, }); -jest.mock('../../../lib/kibana'); +jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../detections/containers/detection_engine/rules'); -jest.mock('../use_add_exception'); -jest.mock('../../../containers/source'); -jest.mock('../use_fetch_or_create_rule_exception_list'); +jest.mock('../../logic/use_add_exception'); +jest.mock('../../../../common/containers/source'); +jest.mock('../../logic/use_fetch_or_create_rule_exception_list'); jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index'); jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async'); jest.mock('@kbn/lists-plugin/public'); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx index 73095d11decaf..78e86dd4f1fa0 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx @@ -45,16 +45,16 @@ import { isNewTermsRule, isThresholdRule, } from '../../../../../common/detection_engine/utils'; -import { useFetchIndex } from '../../../containers/source'; +import { useFetchIndex } from '../../../../common/containers/source'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; import * as i18n from './translations'; -import * as sharedI18n from '../translations'; -import { useKibana } from '../../../lib/kibana'; -import { useAppToasts } from '../../../hooks/use_app_toasts'; -import { useAddOrUpdateException } from '../use_add_exception'; -import { AddExceptionComments } from '../add_exception_comments'; +import * as sharedI18n from '../../utils/translations'; +import { useKibana } from '../../../../common/lib/kibana'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useAddOrUpdateException } from '../../logic/use_add_exception'; +import { ExceptionItemComments } from '../item_comments'; import { enrichExistingExceptionItemWithComments, enrichExceptionItemsWithOS, @@ -62,8 +62,8 @@ import { entryHasNonEcsType, lowercaseHashValues, filterIndexPatterns, -} from '../helpers'; -import { Loader } from '../../loader'; +} from '../../utils/helpers'; +import { Loader } from '../../../../common/components/loader'; import type { ErrorInfo } from '../error_callout'; import { ErrorCallout } from '../error_callout'; @@ -410,7 +410,7 @@ export const EditExceptionFlyout = memo(function EditExceptionFlyout({ - (({ comments }) => { + const { euiTheme } = useEuiTheme(); + + return ( + + + {i18n.exceptionItemCommentsAccordion(comments.length)} +
+ } + arrowDisplay="none" + data-test-subj="exceptionsViewerCommentAccordion" + > + + + + +
+ ); +}); + +ExceptionItemCardComments.displayName = 'ExceptionItemCardComments'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/conditions.test.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/conditions.test.tsx index e31f049e55e13..33fbb2150323c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/conditions.test.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { mount } from 'enzyme'; -import { TestProviders } from '../../../../mock'; -import { ExceptionItemCardConditions } from './exception_item_card_conditions'; +import { TestProviders } from '../../../../common/mock'; +import { ExceptionItemCardConditions } from './conditions'; describe('ExceptionItemCardConditions', () => { it('it includes os condition if one exists', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/conditions.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/conditions.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/header.test.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/header.test.tsx index fe8811152e2e1..2a60e81375745 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/header.test.tsx @@ -11,8 +11,8 @@ import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas import { ThemeProvider } from 'styled-components'; import * as i18n from './translations'; -import { ExceptionItemCardHeader } from './exception_item_card_header'; -import { getMockTheme } from '../../../../lib/kibana/kibana_react.mock'; +import { ExceptionItemCardHeader } from './header'; +import { getMockTheme } from '../../../../common/lib/kibana/kibana_react.mock'; const mockTheme = getMockTheme({ eui: { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/header.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/header.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.test.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.test.tsx index d7a92bff76fae..a7f0d458828f9 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.test.tsx @@ -12,9 +12,9 @@ import { ExceptionItemCard } from '.'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; import { getCommentsArrayMock } from '@kbn/lists-plugin/common/schemas/types/comment.mock'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../../common/mock'; -jest.mock('../../../../lib/kibana'); +jest.mock('../../../../common/lib/kibana'); describe('ExceptionItemCard', () => { it('it renders header, item meta information and conditions', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.tsx similarity index 91% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.tsx index 10c71b47d9db5..a7f672d2e03fe 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.tsx @@ -19,13 +19,13 @@ import React, { useMemo, useCallback } from 'react'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { getFormattedComments } from '../../helpers'; -import type { ExceptionListItemIdentifiers } from '../../types'; +import { getFormattedComments } from '../../utils/helpers'; +import type { ExceptionListItemIdentifiers } from '../../utils/types'; import * as i18n from './translations'; -import { ExceptionItemCardHeader } from './exception_item_card_header'; -import { ExceptionItemCardConditions } from './exception_item_card_conditions'; -import { ExceptionItemCardMetaInfo } from './exception_item_card_meta'; -import type { RuleReferenceSchema } from '../../../../../../common/detection_engine/schemas/response'; +import { ExceptionItemCardHeader } from './header'; +import { ExceptionItemCardConditions } from './conditions'; +import { ExceptionItemCardMetaInfo } from './meta'; +import type { RuleReferenceSchema } from '../../../../../common/detection_engine/schemas/response'; export interface ExceptionItemProps { loadingItemIds: ExceptionListItemIdentifiers[]; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.test.tsx similarity index 97% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.test.tsx index abcae47cee74b..c755321f5f4ea 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.test.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { mount } from 'enzyme'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; -import { TestProviders } from '../../../../mock'; -import { ExceptionItemCardMetaInfo } from './exception_item_card_meta'; +import { ExceptionItemCardMetaInfo } from './meta'; +import { TestProviders } from '../../../../common/mock'; describe('ExceptionItemCardMetaInfo', () => { it('it renders item creation info', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.tsx similarity index 90% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.tsx index ba47fef0af2a1..9bcbb353244e1 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.tsx @@ -22,13 +22,13 @@ import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-t import styled from 'styled-components'; import * as i18n from './translations'; -import { FormattedDate } from '../../../formatted_date'; -import { LinkAnchor } from '../../../links'; -import { APP_UI_ID, SecurityPageName } from '../../../../../../common/constants'; -import { useKibana } from '../../../../lib/kibana'; -import { getRuleDetailsUrl } from '../../../link_to/redirect_to_detection_engine'; -import { useGetSecuritySolutionUrl } from '../../../link_to'; -import type { RuleReferenceSchema } from '../../../../../../common/detection_engine/schemas/response'; +import { FormattedDate } from '../../../../common/components/formatted_date'; +import { LinkAnchor } from '../../../../common/components/links'; +import { APP_UI_ID, SecurityPageName } from '../../../../../common/constants'; +import { useKibana } from '../../../../common/lib/kibana'; +import { getRuleDetailsUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; +import { useGetSecuritySolutionUrl } from '../../../../common/components/link_to'; +import type { RuleReferenceSchema } from '../../../../../common/detection_engine/schemas/response'; const StyledFlexItem = styled(EuiFlexItem)` border-right: 1px solid #d3dae6; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/translations.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/translations.ts diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx similarity index 88% rename from x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx index aa927eef34d76..c83e729669813 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx @@ -18,11 +18,11 @@ import { EuiText, } from '@elastic/eui'; import type { Comment } from '@kbn/securitysolution-io-ts-list-types'; -import * as i18n from './translations'; -import { useCurrentUser } from '../../lib/kibana'; -import { getFormattedComments } from './helpers'; +import * as i18n from '../../utils/translations'; +import { useCurrentUser } from '../../../../common/lib/kibana'; +import { getFormattedComments } from '../../utils/helpers'; -interface AddExceptionCommentsProps { +interface ExceptionItemCommentsProps { exceptionItemComments?: Comment[]; newCommentValue: string; newCommentOnChange: (value: string) => void; @@ -45,11 +45,11 @@ const CommentAccordion = styled(EuiAccordion)` `} `; -export const AddExceptionComments = memo(function AddExceptionComments({ +export const ExceptionItemComments = memo(function ExceptionItemComments({ exceptionItemComments, newCommentValue, newCommentOnChange, -}: AddExceptionCommentsProps) { +}: ExceptionItemCommentsProps) { const [shouldShowComments, setShouldShowComments] = useState(false); const currentUser = useCurrentUser(); @@ -71,7 +71,7 @@ export const AddExceptionComments = memo(function AddExceptionComments({ const commentsAccordionTitle = useMemo(() => { if (exceptionItemComments && exceptionItemComments.length > 0) { return ( - + {!shouldShowComments ? i18n.COMMENTS_SHOW(exceptionItemComments.length) : i18n.COMMENTS_HIDE(exceptionItemComments.length)} @@ -97,7 +97,7 @@ export const AddExceptionComments = memo(function AddExceptionComments({ id={'add-exception-comments-accordion'} buttonClassName={COMMENT_ACCORDION_BUTTON_CLASS_NAME} buttonContent={commentsAccordionTitle} - data-test-subj="addExceptionCommentsAccordion" + data-test-subj="ExceptionItemCommentsAccordion" onToggle={(isOpen) => handleTriggerOnClick(isOpen)} > diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_add_exception.test.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_add_exception.test.tsx index a451ee9e4963f..36446f9ca2a4b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_add_exception.test.tsx @@ -9,7 +9,7 @@ import type { RenderHookResult } from '@testing-library/react-hooks'; import { act, renderHook } from '@testing-library/react-hooks'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { coreMock } from '@kbn/core/public/mocks'; -import { KibanaServices } from '../../lib/kibana'; +import { KibanaServices } from '../../../common/lib/kibana'; import * as alertsApi from '../../../detections/containers/detection_engine/alerts/api'; import * as listsApi from '@kbn/securitysolution-list-api'; @@ -23,7 +23,7 @@ import type { CreateExceptionListItemSchema, UpdateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; -import { TestProviders } from '../../mock'; +import { TestProviders } from '../../../common/mock'; import type { UseAddOrUpdateExceptionProps, ReturnUseAddOrUpdateException, @@ -33,7 +33,7 @@ import { useAddOrUpdateException } from './use_add_exception'; const mockKibanaHttpService = coreMock.createStart().http; const mockKibanaServices = KibanaServices.get as jest.Mock; -jest.mock('../../lib/kibana'); +jest.mock('../../../common/lib/kibana'); jest.mock('@kbn/securitysolution-list-api'); const fetchMock = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_add_exception.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_add_exception.tsx index c06f4928ad894..88556d305bb03 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_add_exception.tsx @@ -22,8 +22,8 @@ import { } from '../../../detections/components/alerts_table/default_config'; import { getQueryFilter } from '../../../../common/detection_engine/get_query_filter'; import type { Index } from '../../../../common/detection_engine/schemas/common/schemas'; -import { formatExceptionItemForUpdate, prepareExceptionItemsForBulkClose } from './helpers'; -import { useKibana } from '../../lib/kibana'; +import { formatExceptionItemForUpdate, prepareExceptionItemsForBulkClose } from '../utils/helpers'; +import { useKibana } from '../../../common/lib/kibana'; /** * Adds exception items to the list. Also optionally closes alerts. diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_fetch_or_create_rule_exception_list.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_fetch_or_create_rule_exception_list.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_fetch_or_create_rule_exception_list.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_fetch_or_create_rule_exception_list.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_find_references.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_find_references.tsx similarity index 91% rename from x-pack/plugins/security_solution/public/common/components/exceptions/use_find_references.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_find_references.tsx index 65f0b37a274c0..8fee876c52f1d 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_find_references.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_find_references.tsx @@ -10,9 +10,9 @@ import type { ListArray } from '@kbn/securitysolution-io-ts-list-types'; import type { RuleReferenceSchema } from '../../../../common/detection_engine/schemas/response'; import { findRuleExceptionReferences } from '../../../detections/containers/detection_engine/rules/api'; -import { useKibana, useToasts } from '../../lib/kibana'; +import { useKibana, useToasts } from '../../../common/lib/kibana'; import type { FindRulesReferencedByExceptionsListProp } from '../../../detections/containers/detection_engine/rules/types'; -import * as i18n from './translations'; +import * as i18n from '../utils/translations'; export type ReturnUseFindExceptionListReferences = [boolean, RuleReferences | null]; @@ -20,8 +20,8 @@ export interface RuleReferences { [key: string]: RuleReferenceSchema[]; } /** - * Hook for finding what rules reference a set of exception lists - * @param listReferences array of exception list info stored on a rule + * Hook for finding what rules are referenced by a set of exception lists + * @param ruleExceptionLists array of exception list info stored on a rule */ export const useFindExceptionListReferences = ( ruleExceptionLists: ListArray diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/exceptionable_endpoint_fields.json similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/exceptionable_endpoint_fields.json diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_linux_fields.json b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/exceptionable_linux_fields.json similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_linux_fields.json rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/exceptionable_linux_fields.json diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_windows_mac_fields.json b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/exceptionable_windows_mac_fields.json similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_windows_mac_fields.json rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/exceptionable_windows_mac_fields.json diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx index c7890f99dc44b..e4087567de39c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx @@ -42,7 +42,7 @@ import type { AlertData, Flattened } from './types'; import type { Ecs } from '../../../../common/ecs'; import type { CodeSignature } from '../../../../common/ecs/file'; -import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; +import { WithCopyToClipboard } from '../../../common/lib/clipboard/with_copy_to_clipboard'; import exceptionableLinuxFields from './exceptionable_linux_fields.json'; import exceptionableWindowsMacFields from './exceptionable_windows_mac_fields.json'; import exceptionableEndpointFields from './exceptionable_endpoint_fields.json'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/translations.ts diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/types.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/types.ts diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx index 577ecca52c59e..ce5b7ee9c5de5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx @@ -73,7 +73,6 @@ export const useInvestigateInTimeline = ({ if (exceptionsLists.length > 0) { await getExceptionListsItems({ lists: exceptionsLists, - filterOptions: [], pagination: { page: 0, perPage: 10000, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 852c7d44aab97..d9d3aa6e92574 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -77,7 +77,7 @@ import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_l import { SecurityPageName } from '../../../../../app/types'; import { LinkButton } from '../../../../../common/components/links'; import { useFormatUrl } from '../../../../../common/components/link_to'; -import { ExceptionsViewer } from '../../../../../common/components/exceptions/viewer'; +import { ExceptionsViewer } from '../../../../../detection_engine/rule_exceptions/all_exception_items_table'; import { APP_UI_ID } from '../../../../../../common/constants'; import { useGlobalFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts index bb65b1c9c4933..909930d713473 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts @@ -9,8 +9,14 @@ import expect from '@kbn/expect'; import { EXCEPTION_LIST_URL, EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; -import { getCreateExceptionListItemMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_item_schema.mock'; -import { getCreateExceptionListMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock'; +import { + getCreateExceptionListItemMinimalSchemaMock, + getCreateExceptionListItemMinimalSchemaMockWithoutId, +} from '@kbn/lists-plugin/common/schemas/request/create_exception_list_item_schema.mock'; +import { + getCreateExceptionListMinimalSchemaMock, + getCreateExceptionListDetectionSchemaMock, +} from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { deleteAllExceptions, removeExceptionListItemServerGeneratedProperties } from '../../utils'; @@ -51,6 +57,79 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('should return matching items when search is passed in', async () => { + // create exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListDetectionSchemaMock()) + .expect(200); + + // create exception list items + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getCreateExceptionListItemMinimalSchemaMockWithoutId(), + list_id: getCreateExceptionListDetectionSchemaMock().list_id, + item_id: '1', + entries: [ + { field: 'host.name', value: 'some host', operator: 'included', type: 'match' }, + ], + }) + .expect(200); + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getCreateExceptionListItemMinimalSchemaMockWithoutId(), + item_id: '2', + list_id: getCreateExceptionListDetectionSchemaMock().list_id, + entries: [{ field: 'foo', operator: 'included', type: 'exists' }], + }) + .expect(200); + + const { body } = await supertest + .get( + `${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${ + getCreateExceptionListMinimalSchemaMock().list_id + }&namespace_type=single&page=1&per_page=25&search=host&sort_field=exception-list.created_at&sort_order=desc` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + body.data = [removeExceptionListItemServerGeneratedProperties(body.data[0])]; + expect(body).to.eql({ + data: [ + { + comments: [], + created_by: 'elastic', + description: 'some description', + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'some host', + }, + ], + item_id: '1', + list_id: 'some-list-id', + name: 'some name', + namespace_type: 'single', + os_types: ['windows'], + tags: [], + type: 'simple', + updated_by: 'elastic', + }, + ], + page: 1, + per_page: 25, + total: 1, + }); + }); + it('should return 404 if given a list_id that does not exist', async () => { const { body } = await supertest .get(`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=non_exist`) From 5626524a854df3ced47cd85c1f424f5efa8ae7b9 Mon Sep 17 00:00:00 2001 From: yctercero Date: Thu, 25 Aug 2022 11:16:24 -0700 Subject: [PATCH 08/30] cleanup --- .../src/common/search/index.test.ts | 2 +- .../all_items.test.tsx | 3 - .../all_exception_items_table/all_items.tsx | 3 - .../all_exception_items_table/index.test.tsx | 207 +++++++++++++++++- .../all_exception_items_table/index.tsx | 77 ++----- .../all_exception_items_table/reducer.ts | 34 +-- .../all_exception_items_table/search_bar.tsx | 2 + .../all_exception_items_table/translations.ts | 2 +- .../utility_bar.test.tsx | 2 +- .../all_exception_items_table/utility_bar.tsx | 4 + .../exception_item_card/index.test.tsx | 6 - .../components/exception_item_card/index.tsx | 41 +--- .../logic/use_find_references.tsx | 6 +- .../timeline_actions/alert_context_menu.tsx | 8 +- .../detection_engine/rules/api.test.ts | 37 ++++ .../containers/detection_engine/rules/api.ts | 4 +- .../detection_engine/rules/types.ts | 2 - .../detection_engine/rules/details/index.tsx | 2 +- .../utils/get_formatted_comments.tsx | 2 +- .../event_filters/view/components/form.tsx | 6 +- 20 files changed, 293 insertions(+), 157 deletions(-) diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.test.ts index e18093ebcf0a5..99d73fd5e059a 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.test.ts @@ -15,7 +15,7 @@ import { left } from 'fp-ts/lib/Either'; import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; -describe('searchAfter', () => { +describe('search', () => { test('it will validate a correct search', () => { const payload = 'name:foo'; const decoded = searchOrUndefined.decode(payload); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.test.tsx index f71a07d4abc60..7897b930bbd86 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.test.tsx @@ -31,7 +31,6 @@ describe('ExceptionsViewerItems', () => { { { = ({ exceptions, - loadingItemIds, listType, disableActions, ruleReferences, @@ -66,7 +64,6 @@ const ExceptionItemsViewerComponent: React.FC = ({ { + const r = jest.requireActual('react'); + return { ...r, useReducer: jest.fn() }; +}); + +const sampleExceptionItem = { + _version: 'WzEwMjM4MSwxXQ==', + comments: [], + created_at: '2022-08-18T17:38:09.018Z', + created_by: 'elastic', + description: 'Index - exception list item', + entries: [ + { + field: 'Endpoint.policy.applied.artifacts.global.identifiers.name', + operator: 'included', + type: 'match', + value: 'sdf', + id: '6a62a5fb-a7d7-44bf-942c-a44b69baba63', + }, + ], + id: '863f3cb0-1f1c-11ed-8a48-9982ed15e50b', + item_id: '74eacd42-7617-4d32-9363-3c074a8892fe', + list_id: '9633e7f2-b92c-4a51-ad56-3e69e5e5f517', + name: 'Index - exception list item', + namespace_type: 'single', + os_types: [], + tags: [], + tie_breaker_id: '5ed24b1f-e717-4798-92ac-9eefd33bb9c0', + type: 'simple', + updated_at: '2022-08-18T17:38:09.020Z', + updated_by: 'elastic', + meta: undefined, +}; const getMockRule = (): Rule => ({ ...mockRule('123'), @@ -47,7 +81,19 @@ describe('ExceptionsViewer', () => { (useFindExceptionListReferences as jest.Mock).mockReturnValue([false, null]); }); - it('it renders loader if "loadingList" is true', () => { + it('it renders loading screen when "currentState" is "loading"', () => { + (useReducer as jest.Mock).mockReturnValue([ + { + exceptions: [], + pagination: { pageIndex: 0, pageSize: 25, totalItemCount: 0, pageSizeOptions: [25, 50] }, + currenFlyout: null, + exceptionToEdit: null, + viewerState: 'loading', + exceptionLists: [], + }, + jest.fn(), + ]); + const wrapper = mount( { ); - expect(wrapper.find('[data-test-subj="loadingPanelAllRulesTable"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-loading"]').exists() + ).toBeTruthy(); }); - it('it renders empty prompt if no "exceptionListMeta" passed in', () => { + it('it renders empty search screen when "currentState" is "empty_search"', () => { + (useReducer as jest.Mock).mockReturnValue([ + { + exceptions: [], + pagination: { pageIndex: 0, pageSize: 25, totalItemCount: 0, pageSizeOptions: [25, 50] }, + currenFlyout: null, + exceptionToEdit: null, + viewerState: 'empty_search', + exceptionLists: [], + }, + jest.fn(), + ]); + const wrapper = mount( { ); - expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-emptySearch"]').exists() + ).toBeTruthy(); + }); + + it('it renders no endpoint items screen when "currentState" is "empty" and "listType" is "endpoint"', () => { + (useReducer as jest.Mock).mockReturnValue([ + { + exceptions: [], + pagination: { pageIndex: 0, pageSize: 25, totalItemCount: 0, pageSizeOptions: [25, 50] }, + currenFlyout: null, + exceptionToEdit: null, + viewerState: 'empty', + exceptionLists: [], + }, + jest.fn(), + ]); + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptBody"]').at(0).text()).toEqual( + i18n.EXCEPTION_EMPTY_ENDPOINT_PROMPT_BODY + ); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptButton"]').at(0).text()).toEqual( + i18n.EXCEPTION_EMPTY_PROMPT_ENDPOINT_BUTTON + ); + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-empty-endpoint"]').exists() + ).toBeTruthy(); }); - it('it renders empty prompt if no exception items exist', () => { + it('it renders no exception items screen when "currentState" is "empty" and "listType" is "detection"', () => { + (useReducer as jest.Mock).mockReturnValue([ + { + exceptions: [], + pagination: { pageIndex: 0, pageSize: 25, totalItemCount: 0, pageSizeOptions: [25, 50] }, + currenFlyout: null, + exceptionToEdit: null, + viewerState: 'empty', + exceptionLists: [], + }, + jest.fn(), + ]); + const wrapper = mount( { ); - expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptBody"]').at(0).text()).toEqual( + i18n.EXCEPTION_EMPTY_PROMPT_BODY + ); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptButton"]').at(0).text()).toEqual( + i18n.EXCEPTION_EMPTY_PROMPT_BUTTON + ); + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-empty-detection"]').exists() + ).toBeTruthy(); + }); + + it('it renders add exception flyout if "currentFlyout" is "addException"', () => { + (useReducer as jest.Mock).mockReturnValue([ + { + exceptions: [], + pagination: { pageIndex: 0, pageSize: 25, totalItemCount: 0, pageSizeOptions: [25, 50] }, + currenFlyout: 'addException', + exceptionToEdit: null, + viewerState: null, + exceptionLists: [], + }, + jest.fn(), + ]); + + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="addExceptionItemFlyout"]').exists()).toBeTruthy(); + }); + + it('it renders edit exception flyout if "currentFlyout" is "editException"', () => { + (useReducer as jest.Mock).mockReturnValue([ + { + exceptions: [sampleExceptionItem], + pagination: { pageIndex: 0, pageSize: 25, totalItemCount: 0, pageSizeOptions: [25, 50] }, + currenFlyout: 'editException', + exceptionToEdit: sampleExceptionItem, + viewerState: null, + exceptionLists: [], + }, + jest.fn(), + ]); + + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="editExceptionItemFlyout"]').exists()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx index 75f365aeb2c33..e525cb8f0fa5c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx @@ -48,9 +48,7 @@ const initialState: State = { }, exceptions: [], exceptionToEdit: null, - loadingItemIds: [], - currentModal: null, - exceptionListTypeToEdit: null, + currenFlyout: null, viewerState: 'loading', exceptionLists: [], }; @@ -74,16 +72,7 @@ const ExceptionsViewerComponent = ({ // Reducer state const [ - { - exceptions, - pagination, - loadingItemIds, - currentModal, - exceptionToEdit, - exceptionListTypeToEdit, - viewerState, - exceptionLists, - }, + { exceptions, pagination, currenFlyout, exceptionToEdit, viewerState, exceptionLists }, dispatch, ] = useReducer(allExceptionItemsReducer(), { ...initialState, @@ -130,16 +119,6 @@ const ExceptionsViewerComponent = ({ [dispatch] ); - const setLoadingItemIds = useCallback( - (items: ExceptionListItemIdentifiers[]): void => { - dispatch({ - type: 'updateLoadingItemIds', - items, - }); - }, - [dispatch] - ); - const [_, allReferences] = useFindExceptionListReferences( rule != null ? rule.exceptions_list ?? [] : [] ); @@ -257,23 +236,18 @@ const ExceptionsViewerComponent = ({ ); const handleAddException = useCallback((): void => { - dispatch({ - type: 'updateExceptionListTypeToEdit', - exceptionListType: listType, - }); setFlyoutType('addException'); - }, [listType, setFlyoutType]); + }, [setFlyoutType]); const handleEditException = useCallback( (exception: ExceptionListItemSchema): void => { dispatch({ type: 'updateExceptionToEdit', - lists: exceptionLists, exception, }); setFlyoutType('editException'); }, - [setFlyoutType, exceptionLists] + [setFlyoutType] ); const handleCancelExceptionItemFlyout = useCallback((): void => { @@ -291,7 +265,7 @@ const ExceptionsViewerComponent = ({ const abortCtrl = new AbortController(); try { - setLoadingItemIds([{ id: itemId, namespaceType }]); + setViewerState('loading'); await deleteExceptionListItemById({ http: services.http, @@ -300,10 +274,8 @@ const ExceptionsViewerComponent = ({ signal: abortCtrl.signal, }); - setLoadingItemIds(loadingItemIds.filter(({ id }) => id !== itemId)); + setViewerState(null); } catch (e) { - setLoadingItemIds(loadingItemIds.filter(({ id }) => id !== itemId)); - setViewerState('error'); toasts.addError(e, { @@ -311,7 +283,7 @@ const ExceptionsViewerComponent = ({ }); } }, - [setLoadingItemIds, services.http, loadingItemIds, setViewerState, toasts] + [services.http, setViewerState, toasts] ); // User privileges checks @@ -333,32 +305,32 @@ const ExceptionsViewerComponent = ({ return ( <> - {currentModal === 'editException' && - exceptionToEdit != null && - exceptionListTypeToEdit != null && ( - - )} + {currenFlyout === 'editException' && exceptionToEdit != null && ( + + )} - {currentModal === 'addException' && exceptionListTypeToEdit != null && ( + {currenFlyout === 'addException' && ( )} @@ -382,7 +354,6 @@ const ExceptionsViewerComponent = ({ { - return list !== null && exception.list_id === list.list_id; - }); + const { exception } = action; return { ...state, exceptionToEdit: exception, - exceptionListTypeToEdit: exceptionListToEdit ? exceptionListToEdit.type : null, }; } case 'updateFlyoutOpen': { return { ...state, - currentModal: action.flyoutType, - }; - } - case 'updateExceptionListTypeToEdit': { - return { - ...state, - exceptionListTypeToEdit: action.exceptionListType, + currenFlyout: action.flyoutType, }; } case 'setViewerState': { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx index 03ca172202719..a516e0997e125 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx @@ -43,6 +43,8 @@ const ITEMS_SCHEMA = { interface ExceptionsViewerSearchBarProps { isReadOnly: boolean; + // Exception list type used to determine what type of item is + // being created when "onAddExceptionClick" is invoked listType: ExceptionListTypeEnum; onSearch: (arg: string) => void; onAddExceptionClick: (type: ExceptionListTypeEnum) => void; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts index d2ea6a648c9a4..3b04d7cc168d9 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts @@ -31,7 +31,7 @@ export const EXCEPTION_EMPTY_PROMPT_TITLE = i18n.translate( export const EXCEPTION_EMPTY_PROMPT_BODY = i18n.translate( 'xpack.securitySolution.exceptions.allItems.emptyPromptBody', { - defaultMessage: 'There are no exceptions on your rule. Create your first rule exception.', + defaultMessage: 'There are no exceptions for this rule. Create your first rule exception.', } ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.test.tsx index 18b987aeb9a76..aa604dbfbf001 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { ExceptionsViewerUtility } from './utility_bar'; -import { TestProviders } from '../../../common/mock'; +import { TestProviders } from '../../../../common/mock'; describe('ExceptionsViewerUtility', () => { it('it renders correct item counts', () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.tsx index 75b7782f171ea..a68930a9a40b8 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.tsx @@ -34,9 +34,13 @@ const StyledCondition = styled.span` interface ExceptionsViewerUtilityProps { pagination: ExceptionsPagination; + // Corresponds to last time exception items were fetched lastUpdated: string | number | null; } +/** + * Utilities include exception item counts and group by options + */ const ExceptionsViewerUtilityComponent: React.FC = ({ pagination, lastUpdated, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.test.tsx index a7f0d458828f9..4972090b7c049 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.test.tsx @@ -24,7 +24,6 @@ describe('ExceptionItemCard', () => { { { { { { { - const { euiTheme } = useEuiTheme(); - const handleDelete = useCallback((): void => { onDeleteException({ id: exceptionItem.id, @@ -65,11 +54,6 @@ const ExceptionItemCardComponent = ({ return getFormattedComments(exceptionItem.comments); }, [exceptionItem.comments]); - const disableItemActions = useMemo((): boolean => { - const foundItems = loadingItemIds.some(({ id }) => id === exceptionItem.id); - return disableActions || foundItems; - }, [loadingItemIds, exceptionItem.id, disableActions]); - return ( @@ -96,7 +80,7 @@ const ExceptionItemCardComponent = ({ onClick: handleDelete, }, ]} - disableActions={disableItemActions} + disableActions={disableActions} dataTestSubj="exceptionItemCardHeader" />
@@ -114,24 +98,7 @@ const ExceptionItemCardComponent = ({ dataTestSubj="exceptionItemCardConditions" />
- {formattedComments.length > 0 && ( - - - {i18n.exceptionItemCommentsAccordion(formattedComments.length)} - - } - arrowDisplay="none" - data-test-subj="exceptionsViewerCommentAccordion" - > - - - - - - )} + {formattedComments.length > 0 && }
); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_find_references.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_find_references.tsx index 8fee876c52f1d..f30d5aeb598a0 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_find_references.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_find_references.tsx @@ -10,7 +10,7 @@ import type { ListArray } from '@kbn/securitysolution-io-ts-list-types'; import type { RuleReferenceSchema } from '../../../../common/detection_engine/schemas/response'; import { findRuleExceptionReferences } from '../../../detections/containers/detection_engine/rules/api'; -import { useKibana, useToasts } from '../../../common/lib/kibana'; +import { useToasts } from '../../../common/lib/kibana'; import type { FindRulesReferencedByExceptionsListProp } from '../../../detections/containers/detection_engine/rules/types'; import * as i18n from '../utils/translations'; @@ -26,7 +26,6 @@ export interface RuleReferences { export const useFindExceptionListReferences = ( ruleExceptionLists: ListArray ): ReturnUseFindExceptionListReferences => { - const { http } = useKibana().services; const toasts = useToasts(); const [isLoading, setIsLoading] = useState(false); const [references, setReferences] = useState(null); @@ -50,7 +49,6 @@ export const useFindExceptionListReferences = ( const { references: referencesResults } = await findRuleExceptionReferences({ lists: listRefs, - http, signal: abortCtrl.signal, }); @@ -85,7 +83,7 @@ export const useFindExceptionListReferences = ( isSubscribed = false; abortCtrl.abort(); }; - }, [http, ruleExceptionLists, listRefs, toasts]); + }, [ruleExceptionLists, listRefs, toasts]); return [isLoading, references]; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 41153cb6b188a..116e8a59f1556 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -17,17 +17,17 @@ import { DEFAULT_ACTION_BUTTON_WIDTH } from '@kbn/timelines-plugin/public'; import { useOsqueryContextActionItem } from '../../osquery/use_osquery_context_action_item'; import { OsqueryFlyout } from '../../osquery/osquery_flyout'; import { useRouteSpy } from '../../../../common/utils/route/use_route_spy'; -import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; +import { buildGetAlertByIdQuery } from '../../../../detection_engine/rule_exceptions/utils/helpers'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { EventsTdContent } from '../../../../timelines/components/timeline/styles'; import type { Ecs } from '../../../../../common/ecs'; -import type { AddExceptionFlyoutProps } from '../../../../common/components/exceptions/add_exception_flyout'; -import { AddExceptionFlyout } from '../../../../common/components/exceptions/add_exception_flyout'; +import type { AddExceptionFlyoutProps } from '../../../../detection_engine/rule_exceptions/components/add_exception_flyout'; +import { AddExceptionFlyout } from '../../../../detection_engine/rule_exceptions/components/add_exception_flyout'; import * as i18n from '../translations'; import type { inputsModel, State } from '../../../../common/store'; import { inputsSelectors } from '../../../../common/store'; import { TimelineId } from '../../../../../common/types'; -import type { AlertData, EcsHit } from '../../../../common/components/exceptions/types'; +import type { AlertData, EcsHit } from '../../../../detection_engine/rule_exceptions/utils/types'; import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index'; import { EventFiltersFlyout } from '../../../../management/pages/event_filters/view/components/event_filters_flyout'; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index 48fa12f03565f..63ccd95657b26 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -20,6 +20,7 @@ import { fetchTags, getPrePackagedRulesStatus, previewRule, + findRuleExceptionReferences, } from './api'; import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { @@ -28,6 +29,7 @@ import { } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { getPatchRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema.mock'; import { rulesMock } from './mock'; +import type { FindRulesReferencedByExceptionsListProp } from './types'; const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; @@ -664,4 +666,39 @@ describe('Detections Rules API', () => { expect(resp).toEqual(prePackagedRulesStatus); }); }); + + describe('findRuleExceptionReferences', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(getRulesSchemaMock()); + }); + + test('GETs exception references', async () => { + const payload: FindRulesReferencedByExceptionsListProp[] = [ + { + id: '123', + listId: 'list_id_1', + namespaceType: 'single', + }, + { + id: '456', + listId: 'list_id_2', + namespaceType: 'single', + }, + ]; + await findRuleExceptionReferences({ lists: payload, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith( + '/api/detection_engine/rules/exceptions/_find_references', + { + query: { + ids: '123,456', + list_ids: 'list_id_1,list_id_2', + namespace_types: 'single,single', + }, + method: 'GET', + signal: abortCtrl.signal, + } + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 22a9c4149b36b..27b3434c8ba5b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -378,17 +378,15 @@ export const fetchInstalledIntegrations = async ({ * Fetch info on what exceptions lists are referenced by what rules * * @param lists exception list information needed for making request - * @param http Kibana http service * @param signal to cancel request * * @throws An error if response is not OK */ export const findRuleExceptionReferences = async ({ lists, - http, signal, }: FindRulesReferencedByExceptionsProps): Promise => - http.fetch( + KibanaServices.get().http.fetch( `${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, { method: 'GET', diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index dfe1a2feabcef..e0ff69af508b6 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -7,7 +7,6 @@ import * as t from 'io-ts'; -import type { HttpStart } from '@kbn/core/public'; import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { listArray } from '@kbn/securitysolution-io-ts-list-types'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; @@ -352,6 +351,5 @@ export interface FindRulesReferencedByExceptionsListProp { export interface FindRulesReferencedByExceptionsProps { lists: FindRulesReferencedByExceptionsListProp[]; - http: HttpStart; signal?: AbortSignal; } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index d9d3aa6e92574..c07ab781b9e77 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -77,7 +77,6 @@ import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_l import { SecurityPageName } from '../../../../../app/types'; import { LinkButton } from '../../../../../common/components/links'; import { useFormatUrl } from '../../../../../common/components/link_to'; -import { ExceptionsViewer } from '../../../../../detection_engine/rule_exceptions/all_exception_items_table'; import { APP_UI_ID } from '../../../../../../common/constants'; import { useGlobalFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; @@ -125,6 +124,7 @@ import { } from '../../../../components/alerts_table/alerts_filter_group'; import { useSignalHelpers } from '../../../../../common/containers/sourcerer/use_signal_helpers'; import { HeaderPage } from '../../../../../common/components/header_page'; +import { ExceptionsViewer } from '../../../../../detection_engine/rule_exceptions/components/all_exception_items_table'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/get_formatted_comments.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/get_formatted_comments.tsx index a75d2a76ae704..0d4c8caf892b2 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/get_formatted_comments.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/get_formatted_comments.tsx @@ -10,7 +10,7 @@ import type { EuiCommentProps } from '@elastic/eui'; import { EuiAvatar, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import type { CommentsArray } from '@kbn/securitysolution-io-ts-list-types'; -import { COMMENT_EVENT } from '../../../../common/components/exceptions/translations'; +import { COMMENT_EVENT } from '../../../../detection_engine/rule_exceptions/utils/translations'; import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; const CustomEuiAvatar = styled(EuiAvatar)` diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx index f157a7e4d804e..f3431de72d7ce 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx @@ -29,13 +29,11 @@ import { getExceptionBuilderComponentLazy } from '@kbn/lists-plugin/public'; import type { OnChangeProps } from '@kbn/lists-plugin/public'; import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator'; import type { PolicyData } from '../../../../../../common/endpoint/types'; -import { AddExceptionComments } from '../../../../../common/components/exceptions/add_exception_comments'; import { useFetchIndex } from '../../../../../common/containers/source'; import { Loader } from '../../../../../common/components/loader'; import { useLicense } from '../../../../../common/hooks/use_license'; import { useKibana } from '../../../../../common/lib/kibana'; import type { ArtifactFormComponentProps } from '../../../../components/artifact_list_page'; -import { filterIndexPatterns } from '../../../../../common/components/exceptions/helpers'; import { isArtifactGlobal, getPolicyIdsFromArtifact, @@ -57,6 +55,8 @@ import { ENDPOINT_EVENT_FILTERS_LIST_ID, EVENT_FILTER_LIST_TYPE } from '../../co import type { EffectedPolicySelection } from '../../../../components/effected_policy_select'; import { EffectedPolicySelect } from '../../../../components/effected_policy_select'; import { isGlobalPolicyEffected } from '../../../../components/effected_policy_select/utils'; +import { ExceptionItemComments } from '../../../../../detection_engine/rule_exceptions/components/item_comments'; +import { filterIndexPatterns } from '../../../../../detection_engine/rule_exceptions/utils/helpers'; const OPERATING_SYSTEMS: readonly OperatingSystem[] = [ OperatingSystem.MAC, @@ -316,7 +316,7 @@ export const EventFiltersForm: React.FC ( - Date: Thu, 25 Aug 2022 11:24:31 -0700 Subject: [PATCH 09/30] cleanup unused translations --- .../plugins/translations/translations/fr-FR.json | 14 -------------- .../plugins/translations/translations/ja-JP.json | 14 -------------- .../plugins/translations/translations/zh-CN.json | 14 -------------- 3 files changed, 42 deletions(-) diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b4b72904c02b6..8b77d5d3572c9 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -25464,7 +25464,6 @@ "xpack.securitySolution.eventsViewer.unit": "{totalCount, plural, =1 {événement} other {événements}}", "xpack.securitySolution.exceptions.dissasociateListSuccessText": "La liste d'exceptions ({id}) a été retirée avec succès", "xpack.securitySolution.exceptions.exceptionItem.showCommentsLabel": "Afficher {comments, plural, =1 {commentaire} other {commentaires}} ({comments})", - "xpack.securitySolution.exceptions.exceptionsPaginationLabel": "Éléments par page : {items}", "xpack.securitySolution.exceptions.failedLoadPolicies": "Une erreur s'est produite lors du chargement des politiques : \"{error}\"", "xpack.securitySolution.exceptions.fetch404Error": "La liste d'exceptions associée ({listId}) n'existe plus. Veuillez retirer la liste d'exceptions manquante pour ajouter des exceptions supplémentaires à la règle de détection.", "xpack.securitySolution.exceptions.hideCommentsLabel": "Masquer ({comments}) {comments, plural, =1 {commentaire} other {commentaires}}", @@ -25472,9 +25471,6 @@ "xpack.securitySolution.exceptions.referenceModalDescription": "Cette liste d'exceptions est associée à ({referenceCount}) {referenceCount, plural, =1 {règle} other {règles}}. Le retrait de cette liste d'exceptions supprimera également sa référence des règles associées.", "xpack.securitySolution.exceptions.referenceModalSuccessDescription": "Liste d'exceptions - {listId} - supprimée avec succès.", "xpack.securitySolution.exceptions.showCommentsLabel": "Afficher ({comments}) {comments, plural, =1 {commentaire} other {commentaires}}", - "xpack.securitySolution.exceptions.utilityNumberExceptionsLabel": "Affichage de {items} {items, plural, =1 {exception} other {exceptions}}", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription": "Toutes les exceptions à cette règle sont appliquées à la règle de détection, et non au point de terminaison. Afficher les {ruleSettings} pour plus de détails.", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription": "Toutes les exceptions à cette règle sont appliquées au point de terminaison et à la règle de détection. Afficher les {ruleSettings} pour plus de détails.", "xpack.securitySolution.fieldBrowser.descriptionForScreenReaderOnly": "Description pour le champ {field} :", "xpack.securitySolution.footer.autoRefreshActiveTooltip": "Lorsque l'actualisation automatique est activée, la chronologie vous montrera les {numberOfItems} derniers événements correspondant à votre recherche.", "xpack.securitySolution.formattedNumber.countsLabel": "{mantissa}{scale}{hasRemainder}", @@ -28090,7 +28086,6 @@ "xpack.securitySolution.exceptions.exceptionItem.metaDetailsBy": "par", "xpack.securitySolution.exceptions.exceptionItem.updatedLabel": "Mis à jour", "xpack.securitySolution.exceptions.fetchError": "Erreur lors de la récupération de la liste d'exceptions", - "xpack.securitySolution.exceptions.fieldDescription": "Champ", "xpack.securitySolution.exceptions.modalErrorAccordionText": "Afficher les informations de référence de la règle :", "xpack.securitySolution.exceptions.nameLabel": "Nom", "xpack.securitySolution.exceptions.operatingSystemFullLabel": "Système d'exploitation", @@ -28099,27 +28094,18 @@ "xpack.securitySolution.exceptions.operatingSystemWindows": "Windows", "xpack.securitySolution.exceptions.operatingSystemWindowsAndMac": "Windows et macOS", "xpack.securitySolution.exceptions.paginationNumberOfItemsLabel": "{items} éléments", - "xpack.securitySolution.exceptions.operatorDescription": "Opérateur", - "xpack.securitySolution.exceptions.orDescription": "OR", "xpack.securitySolution.exceptions.referenceModalCancelButton": "Annuler", "xpack.securitySolution.exceptions.referenceModalDeleteButton": "Retirer la liste d'exceptions", "xpack.securitySolution.exceptions.referenceModalTitle": "Retirer la liste d'exceptions", "xpack.securitySolution.exceptions.searchPlaceholder": "par ex. Exemple de liste de noms", "xpack.securitySolution.exceptions.showCommentsLabel": "Afficher ({comments}) {comments, plural, =1 {commentaire} other {commentaires}}", - "xpack.securitySolution.exceptions.utilityRefreshLabel": "Actualiser", - "xpack.securitySolution.exceptions.valueDescription": "Valeur", "xpack.securitySolution.exceptions.viewer.addCommentPlaceholder": "Ajouter un nouveau commentaire...", "xpack.securitySolution.exceptions.viewer.addToClipboard": "Commentaire", "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "Ajouter une exception à une règle", "xpack.securitySolution.exceptions.viewer.addToEndpointListLabel": "Ajouter une exception de point de terminaison", "xpack.securitySolution.exceptions.viewer.deleteExceptionError": "Erreur lors de la suppression de l'exception", - "xpack.securitySolution.exceptions.viewer.emptyPromptBody": "Vous pouvez ajouter des exceptions pour affiner la règle de façon à ce que les alertes de détection ne soient pas créées lorsque les conditions d'exception sont remplies. Les exceptions améliorent la précision de la détection, ce qui peut permettre de réduire le nombre de faux positifs.", - "xpack.securitySolution.exceptions.viewer.emptyPromptTitle": "Cette règle ne possède aucune exception", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription.ruleSettingsLink": "paramètres de règles", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription.ruleSettingsLink": "paramètres de règles", "xpack.securitySolution.exceptions.viewer.fetchingListError": "Erreur lors de la récupération des exceptions", "xpack.securitySolution.exceptions.viewer.fetchTotalsError": "Erreur lors de l'obtention des totaux d'éléments de l'exception", - "xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody": "Aucun résultat n'a été trouvé pour la recherche.", "xpack.securitySolution.exceptions.viewer.searchDefaultPlaceholder": "Champ de recherche (ex : host.name)", "xpack.securitySolution.exitFullScreenButton": "Quitter le plein écran", "xpack.securitySolution.expandedValue.hideTopValues.HideTopValues": "Masquer les valeurs les plus élevées", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2ac8d84108c82..67c6448b9a3a2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25441,7 +25441,6 @@ "xpack.securitySolution.eventsViewer.unit": "{totalCount, plural, other {イベント}}", "xpack.securitySolution.exceptions.dissasociateListSuccessText": "例外リスト({id})が正常に削除されました", "xpack.securitySolution.exceptions.exceptionItem.showCommentsLabel": "{comments, plural, other {件のコメント}}を表示({comments})", - "xpack.securitySolution.exceptions.exceptionsPaginationLabel": "ページごとの項目数:{items}", "xpack.securitySolution.exceptions.failedLoadPolicies": "ポリシーの読み込みエラーが発生しました:\"{error}\"", "xpack.securitySolution.exceptions.fetch404Error": "関連付けられた例外リスト({listId})は存在しません。その他の例外を検出ルールに追加するには、見つからない例外リストを削除してください。", "xpack.securitySolution.exceptions.hideCommentsLabel": "({comments}){comments, plural, other {件のコメント}}を非表示", @@ -25449,9 +25448,6 @@ "xpack.securitySolution.exceptions.referenceModalDescription": "この例外リストは、({referenceCount}) {referenceCount, plural, other {個のルール}}に関連付けられています。この例外リストを削除すると、関連付けられたルールからの参照も削除されます。", "xpack.securitySolution.exceptions.referenceModalSuccessDescription": "例外リスト{listId}が正常に削除されました。", "xpack.securitySolution.exceptions.showCommentsLabel": "({comments}){comments, plural, other {件のコメント}}を表示", - "xpack.securitySolution.exceptions.utilityNumberExceptionsLabel": "{items} {items, plural, other {件の例外}}を表示しています", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription": "このルールのすべての例外は、エンドポイントではなく、検出ルールに適用されます。詳細については、{ruleSettings}を確認してください。", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription": "このルールのすべての例外は、エンドポイントと検出ルールに適用されます。詳細については、{ruleSettings}を確認してください。", "xpack.securitySolution.fieldBrowser.descriptionForScreenReaderOnly": "フィールド {field} の説明:", "xpack.securitySolution.footer.autoRefreshActiveTooltip": "自動更新が有効な間、タイムラインはクエリに一致する最新の {numberOfItems} 件のイベントを表示します。", "xpack.securitySolution.formattedNumber.countsLabel": "{mantissa}{scale}{hasRemainder}", @@ -28066,7 +28062,6 @@ "xpack.securitySolution.exceptions.exceptionItem.metaDetailsBy": "グループ基準", "xpack.securitySolution.exceptions.exceptionItem.updatedLabel": "更新しました", "xpack.securitySolution.exceptions.fetchError": "例外リストの取得エラー", - "xpack.securitySolution.exceptions.fieldDescription": "フィールド", "xpack.securitySolution.exceptions.modalErrorAccordionText": "ルール参照情報を表示:", "xpack.securitySolution.exceptions.operatingSystemFullLabel": "オペレーティングシステム", "xpack.securitySolution.exceptions.operatingSystemLinux": "Linux", @@ -28074,27 +28069,18 @@ "xpack.securitySolution.exceptions.operatingSystemWindows": "Windows", "xpack.securitySolution.exceptions.operatingSystemWindowsAndMac": "WindowsおよびmacOS", "xpack.securitySolution.exceptions.paginationNumberOfItemsLabel": "{items}個の項目", - "xpack.securitySolution.exceptions.operatorDescription": "演算子", - "xpack.securitySolution.exceptions.orDescription": "OR", "xpack.securitySolution.exceptions.referenceModalCancelButton": "キャンセル", "xpack.securitySolution.exceptions.referenceModalDeleteButton": "例外リストを削除", "xpack.securitySolution.exceptions.referenceModalTitle": "例外リストを削除", "xpack.securitySolution.exceptions.searchPlaceholder": "例:例外リスト名", "xpack.securitySolution.exceptions.showCommentsLabel": "({comments}){comments, plural, other {件のコメント}}を表示", - "xpack.securitySolution.exceptions.utilityRefreshLabel": "更新", - "xpack.securitySolution.exceptions.valueDescription": "値", "xpack.securitySolution.exceptions.viewer.addCommentPlaceholder": "新しいコメントを追加...", "xpack.securitySolution.exceptions.viewer.addToClipboard": "コメント", "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "ルール例外の追加", "xpack.securitySolution.exceptions.viewer.addToEndpointListLabel": "エンドポイント例外の追加", "xpack.securitySolution.exceptions.viewer.deleteExceptionError": "例外の削除エラー", - "xpack.securitySolution.exceptions.viewer.emptyPromptBody": "例外を追加してルールを微調整し、例外条件が満たされたときに検出アラートが作成されないようにすることができます。例外により検出の精度が改善します。これにより、誤検出数が減ります。", - "xpack.securitySolution.exceptions.viewer.emptyPromptTitle": "このルールには例外がありません", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription.ruleSettingsLink": "ルール設定", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription.ruleSettingsLink": "ルール設定", "xpack.securitySolution.exceptions.viewer.fetchingListError": "例外の取得エラー", "xpack.securitySolution.exceptions.viewer.fetchTotalsError": "例外項目合計数の取得エラー", - "xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody": "検索結果が見つかりません。", "xpack.securitySolution.exceptions.viewer.searchDefaultPlaceholder": "検索フィールド(例:host.name)", "xpack.securitySolution.exitFullScreenButton": "全画面を終了", "xpack.securitySolution.expandedValue.hideTopValues.HideTopValues": "上位の値を非表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 531f9955cce97..c4763f4a507d0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25472,7 +25472,6 @@ "xpack.securitySolution.eventsViewer.unit": "{totalCount, plural, other {个事件}}", "xpack.securitySolution.exceptions.dissasociateListSuccessText": "例外列表 ({id}) 已成功移除", "xpack.securitySolution.exceptions.exceptionItem.showCommentsLabel": "显示{comments, plural, other {注释}} ({comments})", - "xpack.securitySolution.exceptions.exceptionsPaginationLabel": "每页项数:{items}", "xpack.securitySolution.exceptions.failedLoadPolicies": "加载策略时出错:“{error}”", "xpack.securitySolution.exceptions.fetch404Error": "关联的例外列表 ({listId}) 已不存在。请移除缺少的例外列表,以将其他例外添加到检测规则。", "xpack.securitySolution.exceptions.hideCommentsLabel": "隐藏 ({comments}) 个{comments, plural, other {注释}}", @@ -25480,9 +25479,6 @@ "xpack.securitySolution.exceptions.referenceModalDescription": "此例外列表与 ({referenceCount}) 个{referenceCount, plural, other {规则}}关联。移除此例外列表还将会删除其对关联规则的引用。", "xpack.securitySolution.exceptions.referenceModalSuccessDescription": "例外列表 - {listId} - 已成功删除。", "xpack.securitySolution.exceptions.showCommentsLabel": "显示 ({comments} 个) {comments, plural, other {注释}}", - "xpack.securitySolution.exceptions.utilityNumberExceptionsLabel": "正在显示 {items} 个{items, plural, other {例外}}", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription": "此规则的所有例外将应用到检测规则,而非终端。查看{ruleSettings}以了解详情。", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription": "此规则的所有例外将应用到终端和检测规则。查看{ruleSettings}以了解详情。", "xpack.securitySolution.fieldBrowser.descriptionForScreenReaderOnly": "{field} 字段的描述:", "xpack.securitySolution.footer.autoRefreshActiveTooltip": "自动刷新已启用时,时间线将显示匹配查询的最近 {numberOfItems} 个事件。", "xpack.securitySolution.formattedNumber.countsLabel": "{mantissa}{scale}{hasRemainder}", @@ -28096,7 +28092,6 @@ "xpack.securitySolution.exceptions.exceptionItem.metaDetailsBy": "依据", "xpack.securitySolution.exceptions.exceptionItem.updatedLabel": "已更新", "xpack.securitySolution.exceptions.fetchError": "提取例外列表时出错", - "xpack.securitySolution.exceptions.fieldDescription": "字段", "xpack.securitySolution.exceptions.modalErrorAccordionText": "显示规则引用信息:", "xpack.securitySolution.exceptions.operatingSystemFullLabel": "操作系统", "xpack.securitySolution.exceptions.operatingSystemLinux": "Linux", @@ -28104,27 +28099,18 @@ "xpack.securitySolution.exceptions.operatingSystemWindows": "Windows", "xpack.securitySolution.exceptions.operatingSystemWindowsAndMac": "Windows 和 macOS", "xpack.securitySolution.exceptions.paginationNumberOfItemsLabel": "{items} 项", - "xpack.securitySolution.exceptions.operatorDescription": "运算符", - "xpack.securitySolution.exceptions.orDescription": "OR", "xpack.securitySolution.exceptions.referenceModalCancelButton": "取消", "xpack.securitySolution.exceptions.referenceModalDeleteButton": "移除例外列表", "xpack.securitySolution.exceptions.referenceModalTitle": "移除例外列表", "xpack.securitySolution.exceptions.searchPlaceholder": "例如,示例列表名称", "xpack.securitySolution.exceptions.showCommentsLabel": "显示 ({comments} 个) {comments, plural, other {注释}}", - "xpack.securitySolution.exceptions.utilityRefreshLabel": "刷新", - "xpack.securitySolution.exceptions.valueDescription": "值", "xpack.securitySolution.exceptions.viewer.addCommentPlaceholder": "添加新注释......", "xpack.securitySolution.exceptions.viewer.addToClipboard": "注释", "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "添加规则例外", "xpack.securitySolution.exceptions.viewer.addToEndpointListLabel": "添加终端例外", "xpack.securitySolution.exceptions.viewer.deleteExceptionError": "删除例外时出错", - "xpack.securitySolution.exceptions.viewer.emptyPromptBody": "可以添加例外以微调规则,以便在满足例外条件时不创建检测告警。例外提升检测精确性,从而可以减少误报数。", - "xpack.securitySolution.exceptions.viewer.emptyPromptTitle": "此规则没有例外", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription.ruleSettingsLink": "规则设置", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription.ruleSettingsLink": "规则设置", "xpack.securitySolution.exceptions.viewer.fetchingListError": "提取例外时出错", "xpack.securitySolution.exceptions.viewer.fetchTotalsError": "获取例外项总数时出错", - "xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody": "找不到搜索结果。", "xpack.securitySolution.exceptions.viewer.searchDefaultPlaceholder": "搜索字段(例如:host.name)", "xpack.securitySolution.exitFullScreenButton": "退出全屏", "xpack.securitySolution.expandedValue.hideTopValues.HideTopValues": "隐藏排名最前值", From 7cf98755ef0fdd4019953a53951dea75568d486b Mon Sep 17 00:00:00 2001 From: yctercero Date: Thu, 25 Aug 2022 12:00:46 -0700 Subject: [PATCH 10/30] fixing loading state when item is deleted --- .../components/all_exception_items_table/all_items.tsx | 2 +- .../components/all_exception_items_table/index.tsx | 8 +++++--- .../components/all_exception_items_table/reducer.ts | 9 ++++++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.tsx index e7ebc0c5e4666..f4dd31509e3dc 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.tsx @@ -50,7 +50,7 @@ const ExceptionItemsViewerComponent: React.FC = ({ }): JSX.Element => { return ( <> - {viewerState != null ? ( + {viewerState != null && viewerState !== 'deleting' ? ( Date: Thu, 25 Aug 2022 12:03:17 -0700 Subject: [PATCH 11/30] fix types --- .../services/exception_lists/find_exception_list_item.ts | 4 ++++ x-pack/plugins/translations/translations/fr-FR.json | 1 - x-pack/plugins/translations/translations/ja-JP.json | 1 - 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts index c08057d77ebbe..fc41afd7563c7 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts @@ -15,6 +15,7 @@ import type { PerPageOrUndefined, PitOrUndefined, SearchAfterOrUndefined, + SearchOrUndefined, SortFieldOrUndefined, SortOrderOrUndefined, } from '@kbn/securitysolution-io-ts-list-types'; @@ -29,6 +30,7 @@ interface FindExceptionListItemOptions { page: PageOrUndefined; perPage: PerPageOrUndefined; pit: PitOrUndefined; + search: SearchOrUndefined; sortField: SortFieldOrUndefined; sortOrder: SortOrderOrUndefined; searchAfter: SearchAfterOrUndefined; @@ -42,6 +44,7 @@ export const findExceptionListItem = async ({ page, perPage, pit, + search, searchAfter, sortField, sortOrder, @@ -54,6 +57,7 @@ export const findExceptionListItem = async ({ perPage, pit, savedObjectsClient, + search, searchAfter, sortField, sortOrder, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 8b77d5d3572c9..6a697cbf41bd1 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -28045,7 +28045,6 @@ "xpack.securitySolution.exceptions.commentEventLabel": "a ajouté un commentaire", "xpack.securitySolution.exceptions.dissasociateExceptionListError": "Impossible de retirer la liste d'exceptions", "xpack.securitySolution.exceptions.dissasociateListSuccessText": "La liste d'exceptions ({id}) a été retirée avec succès", - "xpack.securitySolution.exceptions.editButtonLabel": "Modifier", "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "Fermer toutes les alertes qui correspondent à cette exception et ont été générées par cette règle", "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "Fermer toutes les alertes qui correspondent à cette exception et ont été générées par cette règle (les listes et les champs non ECS ne sont pas pris en charge)", "xpack.securitySolution.exceptions.editException.cancel": "Annuler", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 67c6448b9a3a2..74f65b5b21653 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -28022,7 +28022,6 @@ "xpack.securitySolution.exceptions.commentEventLabel": "コメントを追加しました", "xpack.securitySolution.exceptions.dissasociateExceptionListError": "例外リストを削除できませんでした", "xpack.securitySolution.exceptions.dissasociateListSuccessText": "例外リスト({id})が正常に削除されました", - "xpack.securitySolution.exceptions.editButtonLabel": "編集", "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "この例外一致し、このルールによって生成された、すべてのアラートを閉じる", "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "この例外と一致し、このルールによって生成された、すべてのアラートを閉じる(リストと非ECSフィールドはサポートされません)", "xpack.securitySolution.exceptions.editException.cancel": "キャンセル", From 64643b35b6ec5a83c70e4cae07d0df6c9b6539e2 Mon Sep 17 00:00:00 2001 From: yctercero Date: Thu, 25 Aug 2022 12:38:15 -0700 Subject: [PATCH 12/30] remove unused translation --- x-pack/plugins/translations/translations/fr-FR.json | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 6a697cbf41bd1..1d89ccc1430c1 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -28086,7 +28086,6 @@ "xpack.securitySolution.exceptions.exceptionItem.updatedLabel": "Mis à jour", "xpack.securitySolution.exceptions.fetchError": "Erreur lors de la récupération de la liste d'exceptions", "xpack.securitySolution.exceptions.modalErrorAccordionText": "Afficher les informations de référence de la règle :", - "xpack.securitySolution.exceptions.nameLabel": "Nom", "xpack.securitySolution.exceptions.operatingSystemFullLabel": "Système d'exploitation", "xpack.securitySolution.exceptions.operatingSystemLinux": "Linux", "xpack.securitySolution.exceptions.operatingSystemMac": "macOS", From 68afe7a3f9388af0b2e9afa887c3d07cdd4970d2 Mon Sep 17 00:00:00 2001 From: yctercero Date: Fri, 26 Aug 2022 12:58:01 -0700 Subject: [PATCH 13/30] updating rule details page to allow tabs to be a part of url --- .../link_to/redirect_to_detection_engine.tsx | 3 + .../detection_engine/rules/details/index.tsx | 123 ++++++++---------- .../rules/details/translations.ts | 4 +- .../pages/detection_engine/rules/utils.ts | 23 +++- .../security_solution/public/rules/routes.tsx | 24 +++- 5 files changed, 104 insertions(+), 73 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_detection_engine.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_detection_engine.tsx index 7a6ddbec9e88b..5a71984fabfba 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_detection_engine.tsx @@ -14,5 +14,8 @@ export const getRulesUrl = (search?: string) => `${appendSearch(search)}`; export const getRuleDetailsUrl = (detailName: string, search?: string) => `/id/${detailName}${appendSearch(search)}`; +export const getRuleDetailsTabUrl = (detailName: string, tabName: string, search?: string) => + `/id/${detailName}/${tabName}${appendSearch(search)}`; + export const getEditRuleUrl = (detailName: string, search?: string) => `/id/${detailName}/edit${appendSearch(search)}`; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 051e3db28dd7e..16008f14ab76b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -14,8 +14,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, - EuiTab, - EuiTabs, EuiToolTip, EuiWindowEvent, } from '@elastic/eui'; @@ -34,6 +32,7 @@ import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import type { Dispatch } from 'redux'; import { isTab } from '@kbn/timelines-plugin/public'; import type { DataViewListItem } from '@kbn/data-views-plugin/common'; +import { SecuritySolutionTabNavigation } from '../../../../../common/components/navigation'; import { useDeepEqualSelector, useShallowEqualSelector, @@ -145,37 +144,17 @@ const StyledMinHeightTabContainer = styled.div` export enum RuleDetailTabs { alerts = 'alerts', - exceptions = 'exceptions', + exceptions = 'rule_exceptions', executionResults = 'executionResults', executionEvents = 'executionEvents', } -const ruleDetailTabs = [ - { - id: RuleDetailTabs.alerts, - name: detectionI18n.ALERT, - disabled: false, - dataTestSubj: 'alertsTab', - }, - { - id: RuleDetailTabs.exceptions, - name: i18n.EXCEPTIONS_TAB, - disabled: false, - dataTestSubj: 'exceptionsTab', - }, - { - id: RuleDetailTabs.executionResults, - name: i18n.EXECUTION_RESULTS_TAB, - disabled: false, - dataTestSubj: 'executionResultsTab', - }, - { - id: RuleDetailTabs.executionEvents, - name: i18n.EXECUTION_EVENTS_TAB, - disabled: false, - dataTestSubj: 'executionEventsTab', - }, -]; +export const RULE_DETAILS_TAB_NAME: Record = { + [RuleDetailTabs.alerts]: detectionI18n.ALERT, + [RuleDetailTabs.exceptions]: i18n.EXCEPTIONS_TAB, + [RuleDetailTabs.executionResults]: i18n.EXECUTION_RESULTS_TAB, + [RuleDetailTabs.executionEvents]: i18n.EXECUTION_EVENTS_TAB, +}; type DetectionEngineComponentProps = PropsFromRedux; @@ -240,7 +219,10 @@ const RuleDetailsPageComponent: React.FC = ({ } = useSourcererDataView(SourcererScopeName.detections); const loading = userInfoLoading || listsConfigLoading; - const { detailName: ruleId } = useParams<{ detailName: string }>(); + const { detailName: ruleId, tabName: pageTabName } = useParams<{ + detailName: string; + tabName: string; + }>(); const { rule: maybeRule, refresh: refreshRule, @@ -250,7 +232,41 @@ const RuleDetailsPageComponent: React.FC = ({ const { pollForSignalIndex } = useSignalHelpers(); const [rule, setRule] = useState(null); const isLoading = ruleLoading && rule == null; - const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.alerts); + + const ruleDetailTabs = useMemo( + () => [ + { + id: RuleDetailTabs.alerts, + name: RULE_DETAILS_TAB_NAME[RuleDetailTabs.alerts], + disabled: false, + dataTestSubj: 'alertsTab', + href: `/rules/id/${ruleId}/${RuleDetailTabs.alerts}`, + }, + { + id: RuleDetailTabs.exceptions, + name: RULE_DETAILS_TAB_NAME[RuleDetailTabs.exceptions], + disabled: false, + dataTestSubj: 'exceptionsTab', + href: `/rules/id/${ruleId}/${RuleDetailTabs.exceptions}`, + }, + { + id: RuleDetailTabs.executionResults, + name: RULE_DETAILS_TAB_NAME[RuleDetailTabs.executionResults], + disabled: !isExistingRule, + dataTestSubj: 'executionResultsTab', + href: `/rules/id/${ruleId}/${RuleDetailTabs.executionResults}`, + }, + { + id: RuleDetailTabs.executionEvents, + name: RULE_DETAILS_TAB_NAME[RuleDetailTabs.executionEvents], + disabled: !isExistingRule, + dataTestSubj: 'executionEventsTab', + href: `/rules/id/${ruleId}/${RuleDetailTabs.executionEvents}`, + }, + ], + [isExistingRule, ruleId] + ); + const [pageTabs, setTabs] = useState(ruleDetailTabs); const { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData } = rule != null @@ -360,19 +376,16 @@ const RuleDetailsPageComponent: React.FC = ({ useEffect(() => { let visibleTabs = ruleDetailTabs; - let currentTab = RuleDetailTabs.alerts; if (!hasIndexRead) { visibleTabs = visibleTabs.filter(({ id }) => id !== RuleDetailTabs.alerts); - currentTab = RuleDetailTabs.exceptions; } if (!ruleExecutionSettings.extendedLogging.isEnabled) { visibleTabs = visibleTabs.filter(({ id }) => id !== RuleDetailTabs.executionEvents); } setTabs(visibleTabs); - setRuleDetailTab(currentTab); - }, [hasIndexRead, ruleExecutionSettings]); + }, [hasIndexRead, ruleDetailTabs, ruleExecutionSettings]); const showUpdating = useMemo( () => isLoadingIndexPattern || isAlertsLoading || loading, @@ -479,30 +492,6 @@ const RuleDetailsPageComponent: React.FC = ({ [alertDefaultFilters, filters] ); - const tabs = useMemo( - () => ( - - {pageTabs.map((tab) => ( - setRuleDetailTab(tab.id)} - isSelected={tab.id === ruleDetailTab} - disabled={ - tab.disabled || - ((tab.id === RuleDetailTabs.executionResults || - tab.id === RuleDetailTabs.executionEvents) && - !isExistingRule) - } - key={tab.id} - data-test-subj={tab.dataTestSubj} - > - {tab.name} - - ))} - - ), - [isExistingRule, ruleDetailTab, setRuleDetailTab, pageTabs] - ); - const lastExecution = rule?.execution_summary?.last_execution; const lastExecutionStatus = lastExecution?.status; const lastExecutionDate = lastExecution?.date ?? ''; @@ -664,10 +653,6 @@ const RuleDetailsPageComponent: React.FC = ({ [containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable] ); - const selectAlertsTabCallback = useCallback(() => { - setRuleDetailTab(RuleDetailTabs.alerts); - }, []); - if ( redirectToDetections( isSignalIndexExists, @@ -808,11 +793,11 @@ const RuleDetailsPageComponent: React.FC = ({ - {tabs} + - {ruleDetailTab === RuleDetailTabs.alerts && hasIndexRead && ( + {pageTabName === RuleDetailTabs.alerts && hasIndexRead && ( <> @@ -862,7 +847,7 @@ const RuleDetailsPageComponent: React.FC = ({ )} )} - {ruleDetailTab === RuleDetailTabs.exceptions && ( + {pageTabName === RuleDetailTabs.exceptions && ( = ({ onRuleChange={refreshRule} /> )} - {ruleDetailTab === RuleDetailTabs.executionResults && ( - + {pageTabName === RuleDetailTabs.executionResults && ( + {}} /> )} - {ruleDetailTab === RuleDetailTabs.executionEvents && ( + {pageTabName === RuleDetailTabs.executionEvents && ( )} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts index 299c355ffa480..02e8e01a281e9 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts @@ -36,9 +36,9 @@ export const UNKNOWN = i18n.translate( ); export const EXCEPTIONS_TAB = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab', + 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExceptionsTab', { - defaultMessage: 'Exceptions', + defaultMessage: 'Rule exceptions', } ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts index 68a56efc1879b..b03ad5b886987 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts @@ -6,7 +6,10 @@ */ import type { ChromeBreadcrumb } from '@kbn/core/public'; -import { getRuleDetailsUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; +import { + getRuleDetailsTabUrl, + getRuleDetailsUrl, +} from '../../../../common/components/link_to/redirect_to_detection_engine'; import * as i18nRules from './translations'; import type { RouteSpyState } from '../../../../common/utils/route/types'; import { SecurityPageName } from '../../../../app/types'; @@ -14,6 +17,7 @@ import { RULES_PATH } from '../../../../../common/constants'; import type { RuleStepsOrder } from './types'; import { RuleStep } from './types'; import type { GetSecuritySolutionUrl } from '../../../../common/components/link_to'; +import { RuleDetailTabs, RULE_DETAILS_TAB_NAME } from './details'; export const ruleStepsOrder: RuleStepsOrder = [ RuleStep.defineRule, @@ -22,6 +26,10 @@ export const ruleStepsOrder: RuleStepsOrder = [ RuleStep.ruleActions, ]; +const getRuleDetailsTabName = (tabName: string): string => { + return RULE_DETAILS_TAB_NAME[tabName] ?? RULE_DETAILS_TAB_NAME[RuleDetailTabs.alerts]; +}; + const isRuleCreatePage = (pathname: string) => pathname.includes(RULES_PATH) && pathname.includes('/create'); @@ -47,6 +55,19 @@ export const getTrailingBreadcrumbs = ( ]; } + if (params.detailName && params.state?.ruleName && params.tabName) { + breadcrumb = [ + ...breadcrumb, + { + text: getRuleDetailsTabName(params.tabName), + href: getSecuritySolutionUrl({ + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsTabUrl(params.detailName, params.tabName, ''), + }), + }, + ]; + } + if (isRuleCreatePage(params.pathName)) { breadcrumb = [ ...breadcrumb, diff --git a/x-pack/plugins/security_solution/public/rules/routes.tsx b/x-pack/plugins/security_solution/public/rules/routes.tsx index b3680a81a2dba..13adb118030d4 100644 --- a/x-pack/plugins/security_solution/public/rules/routes.tsx +++ b/x-pack/plugins/security_solution/public/rules/routes.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React from 'react'; -import { Switch } from 'react-router-dom'; +import { Redirect, Switch } from 'react-router-dom'; import { Route } from '@kbn/kibana-react-plugin/public'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; @@ -18,6 +18,7 @@ import { RuleDetailsPage } from '../detections/pages/detection_engine/rules/deta import { EditRulePage } from '../detections/pages/detection_engine/rules/edit'; import { useReadonlyHeader } from '../use_readonly_header'; import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper'; +import { SpyRoute } from '../common/utils/route/spy_routes'; const RulesSubRoutes = [ { @@ -30,6 +31,11 @@ const RulesSubRoutes = [ main: RuleDetailsPage, exact: true, }, + { + path: '/rules/id/:detailName/:tabName', + main: RuleDetailsPage, + exact: true, + }, { path: '/rules/create', main: CreateRulePage, @@ -49,6 +55,21 @@ const RulesContainerComponent: React.FC = () => { + {/* Because '/rules/id/:detailName/edit' would match '/rules/id/:detailName/:tabName' need + to add a redirect to ensure that it continues to work as expected */} + ( + + )} + /> {RulesSubRoutes.map((route, index) => ( { ))} + From e8447a406a7415a1f73286d717147d3b0883b229 Mon Sep 17 00:00:00 2001 From: yctercero Date: Fri, 26 Aug 2022 13:39:09 -0700 Subject: [PATCH 14/30] fixed alerts tab navigation from execution results tab --- .../detection_engine/rules/details/index.tsx | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 16008f14ab76b..407e5230d19ab 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -20,7 +20,7 @@ import { import { i18n as i18nTranslate } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { noop } from 'lodash/fp'; +import { noop, omit } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; import type { ConnectedProps } from 'react-redux'; @@ -46,6 +46,7 @@ import { getEditRuleUrl, getRulesUrl, getDetectionEngineUrl, + getRuleDetailsTabUrl, } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../../../../common/components/search_bar'; import { SecuritySolutionPageWrapper } from '../../../../../common/components/page_wrapper'; @@ -125,6 +126,7 @@ import { } from '../../../../components/alerts_table/alerts_filter_group'; import { useSignalHelpers } from '../../../../../common/containers/sourcerer/use_signal_helpers'; import { HeaderPage } from '../../../../../common/components/header_page'; +import type { NavTab } from '../../../../../common/components/navigation/types'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. @@ -234,40 +236,36 @@ const RuleDetailsPageComponent: React.FC = ({ const isLoading = ruleLoading && rule == null; const ruleDetailTabs = useMemo( - () => [ - { + (): Record => ({ + [RuleDetailTabs.alerts]: { id: RuleDetailTabs.alerts, name: RULE_DETAILS_TAB_NAME[RuleDetailTabs.alerts], disabled: false, - dataTestSubj: 'alertsTab', href: `/rules/id/${ruleId}/${RuleDetailTabs.alerts}`, }, - { + [RuleDetailTabs.exceptions]: { id: RuleDetailTabs.exceptions, name: RULE_DETAILS_TAB_NAME[RuleDetailTabs.exceptions], disabled: false, - dataTestSubj: 'exceptionsTab', href: `/rules/id/${ruleId}/${RuleDetailTabs.exceptions}`, }, - { + [RuleDetailTabs.executionResults]: { id: RuleDetailTabs.executionResults, name: RULE_DETAILS_TAB_NAME[RuleDetailTabs.executionResults], disabled: !isExistingRule, - dataTestSubj: 'executionResultsTab', href: `/rules/id/${ruleId}/${RuleDetailTabs.executionResults}`, }, - { + [RuleDetailTabs.executionEvents]: { id: RuleDetailTabs.executionEvents, name: RULE_DETAILS_TAB_NAME[RuleDetailTabs.executionEvents], disabled: !isExistingRule, - dataTestSubj: 'executionEventsTab', href: `/rules/id/${ruleId}/${RuleDetailTabs.executionEvents}`, }, - ], + }), [isExistingRule, ruleId] ); - const [pageTabs, setTabs] = useState(ruleDetailTabs); + const [pageTabs, setTabs] = useState>(ruleDetailTabs); const { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData } = rule != null ? getStepsData({ rule, detailsView: true }) @@ -321,6 +319,13 @@ const RuleDetailsPageComponent: React.FC = ({ return true; }, [actions, rule?.actions]); + const navigateToAlertsTab = useCallback(() => { + navigateToApp(APP_UI_ID, { + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsTabUrl(ruleId ?? '', 'alerts', ''), + }); + }, [navigateToApp, ruleId]); + // persist rule until refresh is complete useEffect(() => { if (maybeRule != null) { @@ -375,16 +380,16 @@ const RuleDetailsPageComponent: React.FC = ({ const ruleExecutionSettings = useRuleExecutionSettings(); useEffect(() => { - let visibleTabs = ruleDetailTabs; + const hiddenTabs = []; if (!hasIndexRead) { - visibleTabs = visibleTabs.filter(({ id }) => id !== RuleDetailTabs.alerts); + hiddenTabs.push(RuleDetailTabs.alerts); } if (!ruleExecutionSettings.extendedLogging.isEnabled) { - visibleTabs = visibleTabs.filter(({ id }) => id !== RuleDetailTabs.executionEvents); + hiddenTabs.push(RuleDetailTabs.executionEvents); } - setTabs(visibleTabs); + setTabs(omit(hiddenTabs, ruleDetailTabs)); }, [hasIndexRead, ruleDetailTabs, ruleExecutionSettings]); const showUpdating = useMemo( @@ -860,7 +865,7 @@ const RuleDetailsPageComponent: React.FC = ({ /> )} {pageTabName === RuleDetailTabs.executionResults && ( - {}} /> + )} {pageTabName === RuleDetailTabs.executionEvents && ( From a70436a8a474733d7f700947c2ff6f13987c8e53 Mon Sep 17 00:00:00 2001 From: yctercero Date: Fri, 26 Aug 2022 13:45:10 -0700 Subject: [PATCH 15/30] remove unused translation --- x-pack/plugins/translations/translations/fr-FR.json | 1 - x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 3 deletions(-) diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index a1db17454109d..ae02468f8b206 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -27150,7 +27150,6 @@ "xpack.securitySolution.detectionEngine.ruleDetails.backToRulesButton": "Règles", "xpack.securitySolution.detectionEngine.ruleDetails.deletedRule": "Règle supprimée", "xpack.securitySolution.detectionEngine.ruleDetails.enableRuleLabel": "Activer", - "xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab": "Exceptions", "xpack.securitySolution.detectionEngine.ruleDetails.pageTitle": "Détails de la règle", "xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionEventsTab": "Événements d’exécution", "xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.actionFieldNotFoundErrorDescription": "Impossible de trouver le champ \"kibana.alert.rule.execution.uuid\" dans l'index des alertes.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9ec2c5ba026da..a077795fb40aa 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -27127,7 +27127,6 @@ "xpack.securitySolution.detectionEngine.ruleDetails.backToRulesButton": "ルール", "xpack.securitySolution.detectionEngine.ruleDetails.deletedRule": "削除されたルール", "xpack.securitySolution.detectionEngine.ruleDetails.enableRuleLabel": "有効にする", - "xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab": "例外", "xpack.securitySolution.detectionEngine.ruleDetails.pageTitle": "ルール詳細", "xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionEventsTab": "実行イベント", "xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.actionFieldNotFoundErrorDescription": "アラートインデックスにフィールド'kibana.alert.rule.execution.uuid'が見つかりません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 915e40765e5aa..569ef21aee6a6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27158,7 +27158,6 @@ "xpack.securitySolution.detectionEngine.ruleDetails.backToRulesButton": "规则", "xpack.securitySolution.detectionEngine.ruleDetails.deletedRule": "已删除规则", "xpack.securitySolution.detectionEngine.ruleDetails.enableRuleLabel": "启用", - "xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab": "例外", "xpack.securitySolution.detectionEngine.ruleDetails.pageTitle": "规则详情", "xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionEventsTab": "执行事件", "xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.actionFieldNotFoundErrorDescription": "在告警索引中找不到字段“kibana.alert.rule.execution.uuid”。", From 871d5afe5a22178dcf0696fff8e26ea26c285b6b Mon Sep 17 00:00:00 2001 From: yctercero Date: Sat, 27 Aug 2022 11:04:41 -0700 Subject: [PATCH 16/30] updates tests --- .../cypress/integration/urls/compatibility.spec.ts | 5 +++++ .../cypress/integration/urls/not_found.spec.ts | 2 +- .../security_solution/cypress/screens/rule_details.ts | 4 ++-- .../pages/detection_engine/rules/details/index.tsx | 6 ++++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/urls/compatibility.spec.ts b/x-pack/plugins/security_solution/cypress/integration/urls/compatibility.spec.ts index 29f7190488f40..ba3772c3b0d3a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/urls/compatibility.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/urls/compatibility.spec.ts @@ -63,6 +63,11 @@ describe('URL compatibility', () => { cy.url().should('include', ruleDetailsUrl(RULE_ID)); }); + it('Redirects to rule details alerts tab from old Detections rule details URL', () => { + visit(ruleDetailsUrl(RULE_ID)); + cy.url().should('include', `${ruleDetailsUrl(RULE_ID)}/alerts`); + }); + it('Redirects to rule edit from old Detections rule edit URL', () => { visit(detectionRuleEditUrl(RULE_ID)); cy.url().should('include', ruleEditUrl(RULE_ID)); diff --git a/x-pack/plugins/security_solution/cypress/integration/urls/not_found.spec.ts b/x-pack/plugins/security_solution/cypress/integration/urls/not_found.spec.ts index 8bd3e6a9ed93b..ee4dda2e2181b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/urls/not_found.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/urls/not_found.spec.ts @@ -51,7 +51,7 @@ describe('Display not found page', () => { }); it('navigates to the rules details page with incorrect link', () => { - visit(`${ruleDetailsUrl(mockRuleId)}/randomUrl`); + visit(`${ruleDetailsUrl(mockRuleId)}/test/randomUrl`); cy.get(NOT_FOUND).should('exist'); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index 4aed5286ee1f1..989353cf7a253 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -16,7 +16,7 @@ export const ABOUT_DETAILS = export const ADDITIONAL_LOOK_BACK_DETAILS = 'Additional look-back time'; -export const ALERTS_TAB = '[data-test-subj="alertsTab"]'; +export const ALERTS_TAB = '[data-test-subj="navigation-alerts"]'; export const ANOMALY_SCORE_DETAILS = 'Anomaly score'; @@ -31,7 +31,7 @@ export const DETAILS_DESCRIPTION = '.euiDescriptionList__description'; export const DETAILS_TITLE = '.euiDescriptionList__title'; -export const EXCEPTIONS_TAB = '[data-test-subj="exceptionsTab"]'; +export const EXCEPTIONS_TAB = '[data-test-subj="navigation-rule_exceptions"]'; export const FALSE_POSITIVES_DETAILS = 'False positive examples'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 407e5230d19ab..edcbb6a4a610b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -265,7 +265,7 @@ const RuleDetailsPageComponent: React.FC = ({ [isExistingRule, ruleId] ); - const [pageTabs, setTabs] = useState>(ruleDetailTabs); + const [pageTabs, setTabs] = useState>>(ruleDetailTabs); const { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData } = rule != null ? getStepsData({ rule, detailsView: true }) @@ -389,7 +389,9 @@ const RuleDetailsPageComponent: React.FC = ({ hiddenTabs.push(RuleDetailTabs.executionEvents); } - setTabs(omit(hiddenTabs, ruleDetailTabs)); + const tabs = omit>(hiddenTabs, ruleDetailTabs); + + setTabs(tabs); }, [hasIndexRead, ruleDetailTabs, ruleExecutionSettings]); const showUpdating = useMemo( From 987f4a31b17ea2d2a0f8af023fab4cc424d9619d Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Sun, 28 Aug 2022 17:07:36 -0700 Subject: [PATCH 17/30] fixing cypress tests --- .../security_solution/cypress/screens/rule_details.ts | 2 +- .../security_solution/cypress/tasks/rule_details.ts | 7 +------ .../pages/detection_engine/rules/details/index.tsx | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index 989353cf7a253..1fb9df6508917 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -31,7 +31,7 @@ export const DETAILS_DESCRIPTION = '.euiDescriptionList__description'; export const DETAILS_TITLE = '.euiDescriptionList__title'; -export const EXCEPTIONS_TAB = '[data-test-subj="navigation-rule_exceptions"]'; +export const EXCEPTIONS_TAB = '[data-test-subj="navigation-ruleExceptions"]'; export const FALSE_POSITIVES_DETAILS = 'False positive examples'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index c021ead16a3f0..dcc4163fa9bf5 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -89,12 +89,7 @@ export const goToAlertsTab = () => { }; export const goToExceptionsTab = () => { - cy.root() - .pipe(($el) => { - $el.find(EXCEPTIONS_TAB).trigger('click'); - return $el.find(ADD_EXCEPTIONS_BTN); - }) - .should('be.visible'); + cy.get(EXCEPTIONS_TAB).click(); }; export const editException = () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index edcbb6a4a610b..985f4007ff8f8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -146,7 +146,7 @@ const StyledMinHeightTabContainer = styled.div` export enum RuleDetailTabs { alerts = 'alerts', - exceptions = 'rule_exceptions', + exceptions = 'ruleExceptions', executionResults = 'executionResults', executionEvents = 'executionEvents', } From f524351fbb7200ddc2fed01b4c144b469454f9e7 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Sun, 28 Aug 2022 17:46:01 -0700 Subject: [PATCH 18/30] cleanup --- 4688.txt | 307 ++++++++++++++++++ .../__tests__/enumerate_patterns.test.js | 4 +- .../exception_lists/exception_list_client.ts | 2 + .../exception_list_client_types.ts | 2 + ...on_product_no_results_magnifying_glass.svg | 1 + .../empty_viewer_state.tsx | 155 ++++----- .../all_exception_items_table/index.tsx | 32 +- .../search_bar.test.tsx | 3 + .../all_exception_items_table/search_bar.tsx | 3 + .../all_exception_items_table/translations.ts | 11 +- .../detection_engine/rules/details/index.tsx | 2 +- 11 files changed, 425 insertions(+), 97 deletions(-) create mode 100644 4688.txt create mode 100644 x-pack/plugins/security_solution/public/common/images/illustration_product_no_results_magnifying_glass.svg diff --git a/4688.txt b/4688.txt new file mode 100644 index 0000000000000..33eca94bd7c2d --- /dev/null +++ b/4688.txt @@ -0,0 +1,307 @@ + +{ + "_index": ".ds-logs-windows_custom_domain-default-2022.08.04-000018", + "_id": "ZgknxoIBrnX8mZ5R-j-7", + "_version": 1, + "_score": 0, + "_source": { + "agent": { + "name": "WEC-Server", + "id": "69566376-69e2-4630-b061-faeb1094d11e", + "ephemeral_id": "bd6a5bfb-5763-4026-85b8-94d037c993fd", + "type": "filebeat", + "version": "8.3.3" + }, + "winlog": { + "computer_name": "ServerEventCreate.domain", + "process": { + "pid": 4, + "thread": { + "id": 16536 + } + }, + "keywords": [ + "Audit Success" + ], + "channel": "Security", + "event_data": { + "MandatoryLabel": "S-1-16-8192", + "ParentProcessName": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + "NewProcessId": "0x2eb0", + "TokenElevationType": "%%1936", + "SubjectLogonId": "0x134ad751", + "NewProcessName": "C:\\Windows\\System32\\whoami.exe", + "TargetLogonId": "0x0", + "SubjectUserName": "User.name", + "CommandLine": "\"C:\\Windows\\system32\\whoami.exe\" /priv", + "SubjectDomainName": "Domain", + "TargetUserName": "-", + "ProcessId": "0x5e88", + "TargetDomainName": "-", + "SubjectUserSid": "S-1-5-21-321992054-2886514165-123123123-1160", + "TargetUserSid": "S-1-0-0" + }, + "opcode": "Info", + "version": 2, + "record_id": 10111571, + "event_id": "4688", + "task": "Process Creation", + "provider_guid": "{54849625-5478-4994-a5ba-3e3b0328c30d}", + "api": "wineventlog", + "provider_name": "Microsoft-Windows-Security-Auditing" + }, + "log": { + "level": "information" + }, + "elastic_agent": { + "id": "69566376-69e2-4630-b061-faeb1094d11e", + "version": "8.3.3", + "snapshot": false + }, + "message": "A new process has been created.\n\nCreator Subject:\n\tSecurity ID:\t\tS-1-5-21-321992054-2886514165-175804892-1160\n\tAccount Name:\t\tuser.name\n\tAccount Domain:\t\tdomain\n\tLogon ID:\t\t0x134AD751\n\nTarget Subject:\n\tSecurity ID:\t\tS-1-0-0\n\tAccount Name:\t\t-\n\tAccount Domain:\t\t-\n\tLogon ID:\t\t0x0\n\nProcess Information:\n\tNew Process ID:\t\t0x2eb0\n\tNew Process Name:\tC:\\Windows\\System32\\whoami.exe\n\tToken Elevation Type:\t%%1936\n\tMandatory Label:\t\tS-1-16-8192\n\tCreator Process ID:\t0x5e88\n\tCreator Process Name:\tC:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\n\tProcess Command Line:\t\"C:\\Windows\\system32\\whoami.exe\" /priv\n\nToken Elevation Type indicates the type of token that was assigned to the new process in accordance with User Account Control policy.\n\nType 1 is a full token with no privileges removed or groups disabled. A full token is only used if User Account Control is disabled or if the user is the built-in Administrator account or a service account.\n\nType 2 is an elevated token with no privileges removed or groups disabled. An elevated token is used when User Account Control is enabled and the user chooses to start the program using Run as administrator. An elevated token is also used when an application is configured to always require administrative privilege or to always require maximum privilege, and the user is a member of the Administrators group.\n\nType 3 is a limited token with administrative privileges removed and administrative groups disabled. The limited token is used when User Account Control is enabled, the application does not require administrative privilege, and the user does not choose to start the program using Run as administrator.", + "input": { + "type": "winlog" + }, + "@timestamp": "2022-08-22T15:21:03.631Z", + "ecs": { + "version": "8.0.0" + }, + "parser": { + "version": "1.0" + }, + "data_stream": { + "namespace": "default", + "type": "logs", + "dataset": "windows_custom_domain" + }, + "host": { + "hostname": "Wec-Server", + "os": { + "build": "17763.3165", + "kernel": "10.0.17763.3165 (WinBuild.160101.0800)", + "name": "Windows Server 2019 Datacenter", + "type": "windows", + "family": "windows", + "version": "10.0", + "platform": "windows" + }, + "ip": [ + "fe80::b92d:654c:2151:8b7b", + "10.10.10.10" + ], + "name": "ServerEventCreated.domain", + "id": "1589e245-c480-4426-8867-2c13e9fc009e", + "mac": [ + "00:15:5d:f6:8a:0d" + ], + "architecture": "x86_64" + }, + "event": { + "code": "4688", + "provider": "Microsoft-Windows-Security-Auditing", + "created": "2022-08-22T15:24:43.970Z", + "kind": "event", + "action": "Process Creation", + "dataset": "windows_custom_domain", + "outcome": "success" + } + }, + "fields": { + "elastic_agent.version": [ + "8.3.3" + ], + "winlog.event_data.NewProcessName": [ + "C:\\Windows\\System32\\whoami.exe" + ], + "winlog.provider_guid": [ + "{54849625-5478-4994-a5ba-3e3b0328c30d}" + ], + "winlog.provider_name": [ + "Microsoft-Windows-Security-Auditing" + ], + "host.hostname": [ + "WEC-Server" + ], + "winlog.computer_name": [ + "ServerEventCreated.domain" + ], + "host.mac": [ + "00:15:5d:f6:8a:0d" + ], + "winlog.process.pid": [ + 4 + ], + "host.os.version": [ + "10.0" + ], + "winlog.keywords": [ + "Audit Success" + ], + "winlog.record_id": [ + 10111571 + ], + "winlog.event_data.CommandLine": [ + "\"C:\\Windows\\system32\\whoami.exe\" /priv" + ], + "host.os.name": [ + "Windows Server 2019 Datacenter" + ], + "log.level": [ + "information" + ], + "agent.name": [ + "WEC-Server" + ], + "winlog.event_data.ParentProcessName": [ + "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" + ], + "host.name": [ + "ServerEventCreated.domain" + ], + "event.kind": [ + "event" + ], + "winlog.event_data.NewProcessId": [ + "0x2eb0" + ], + "event.outcome": [ + "success" + ], + "winlog.version": [ + 2 + ], + "winlog.event_data.TargetUserName": [ + "-" + ], + "host.os.type": [ + "windows" + ], + "winlog.event_data.TargetUserSid": [ + "S-1-0-0" + ], + "input.type": [ + "winlog" + ], + "data_stream.type": [ + "logs" + ], + "host.architecture": [ + "x86_64" + ], + "event.provider": [ + "Microsoft-Windows-Security-Auditing" + ], + "event.code": [ + "4688" + ], + "agent.id": [ + "69566376-69e2-4630-b061-faeb1094d11e" + ], + "ecs.version": [ + "8.0.0" + ], + "winlog.event_data.TokenElevationType": [ + "%%1936" + ], + "event.created": [ + "2022-08-22T15:24:43.970Z" + ], + "agent.version": [ + "8.3.3" + ], + "host.os.family": [ + "windows" + ], + "winlog.event_data.SubjectUserSid": [ + "S-1-5-21-321992054-2886514165-17512312423-1160" + ], + "winlog.process.thread.id": [ + 16536 + ], + "host.os.build": [ + "17763.3165" + ], + "host.ip": [ + "fe80::b92d:654c:2151:8b7b", + "10.10.10.10" + ], + "agent.type": [ + "filebeat" + ], + "winlog.event_data.SubjectLogonId": [ + "0x134ad751" + ], + "winlog.event_data.TargetLogonId": [ + "0x0" + ], + "host.os.kernel": [ + "10.0.17763.3165 (WinBuild.160101.0800)" + ], + "winlog.api": [ + "wineventlog" + ], + "elastic_agent.snapshot": [ + false + ], + "winlog.event_data.ProcessId": [ + "0x5e88" + ], + "host.id": [ + "1589e245-c480-4426-8867-2c13e9fc009e" + ], + "winlog.task": [ + "Process Creation" + ], + "elastic_agent.id": [ + "69566376-69e2-4630-b061-faeb1094d11e" + ], + "winlog.event_data.SubjectUserName": [ + "user.name" + ], + "data_stream.namespace": [ + "default" + ], + "parser.version": [ + "1.0" + ], + "winlog.event_id": [ + "4688" + ], + "message": [ + "A new process has been created.\n\nCreator Subject:\n\tSecurity ID:\t\tS-1-5-21-321992054-2886514165-2131243324-1160\n\tAccount Name:\t\t1user.name\n\tAccount Domain:\t\tdomain\n\tLogon ID:\t\t0x134AD751\n\nTarget Subject:\n\tSecurity ID:\t\tS-1-0-0\n\tAccount Name:\t\t-\n\tAccount Domain:\t\t-\n\tLogon ID:\t\t0x0\n\nProcess Information:\n\tNew Process ID:\t\t0x2eb0\n\tNew Process Name:\tC:\\Windows\\System32\\whoami.exe\n\tToken Elevation Type:\t%%1936\n\tMandatory Label:\t\tS-1-16-8192\n\tCreator Process ID:\t0x5e88\n\tCreator Process Name:\tC:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\n\tProcess Command Line:\t\"C:\\Windows\\system32\\whoami.exe\" /priv\n\nToken Elevation Type indicates the type of token that was assigned to the new process in accordance with User Account Control policy.\n\nType 1 is a full token with no privileges removed or groups disabled. A full token is only used if User Account Control is disabled or if the user is the built-in Administrator account or a service account.\n\nType 2 is an elevated token with no privileges removed or groups disabled. An elevated token is used when User Account Control is enabled and the user chooses to start the program using Run as administrator. An elevated token is also used when an application is configured to always require administrative privilege or to always require maximum privilege, and the user is a member of the Administrators group.\n\nType 3 is a limited token with administrative privileges removed and administrative groups disabled. The limited token is used when User Account Control is enabled, the application does not require administrative privilege, and the user does not choose to start the program using Run as administrator." + ], + "event.action": [ + "Process Creation" + ], + "@timestamp": [ + "2022-08-22T15:21:03.631Z" + ], + "winlog.channel": [ + "Security" + ], + "host.os.platform": [ + "windows" + ], + "winlog.event_data.MandatoryLabel": [ + "S-1-16-8192" + ], + "winlog.event_data.TargetDomainName": [ + "-" + ], + "data_stream.dataset": [ + "windows_custom_domain" + ], + "winlog.opcode": [ + "Info" + ], + "agent.ephemeral_id": [ + "bd6a5bfb-5763-4026-85b8-94d037c993fd" + ], + "winlog.event_data.SubjectDomainName": [ + "domain" + ], + "event.dataset": [ + "windows_custom_domain" + ] + } +} \ No newline at end of file diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js index 8ec97d3fbd385..407fc32e5a8f9 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js @@ -37,13 +37,13 @@ describe(`enumeratePatterns`, () => { 'src/plugins/charts/common/static/color_maps/color_maps.ts kibana-app' ); }); - it(`should resolve x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/translations.ts to kibana-security`, () => { + it(`should resolve x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/translations.ts to kibana-security`, () => { const short = 'x-pack/plugins/security_solution'; const actual = enumeratePatterns(REPO_ROOT)(log)(new Map([[short, ['kibana-security']]])); expect( actual[0].includes( - `${short}/public/common/components/exceptions/edit_exception_flyout/translations.ts kibana-security` + `${short}/public/detection_engine/rule_exceptions/components/edit_exception_flyout/translations.ts` ) ).toBe(true); }); diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index 55f76f2135a80..5f1f4c018586b 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -717,6 +717,7 @@ export class ExceptionListClient { perPage, pit, page, + search, searchAfter, sortField, sortOrder, @@ -750,6 +751,7 @@ export class ExceptionListClient { perPage, pit, savedObjectsClient, + search, searchAfter, sortField, sortOrder, diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index 336c7f38208ff..2e4e63d9a1c18 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -362,6 +362,8 @@ export interface FindExceptionListItemOptions { perPage: PerPageOrUndefined; /** The Point in Time (pit) id if there is one, otherwise "undefined" can be send in */ pit?: PitOrUndefined; + /** The simple search parameter if there is one, otherwise "undefined" can be sent in */ + search?: SearchOrUndefined; /** The search_after parameter if there is one, otherwise "undefined" can be sent in */ searchAfter?: SearchAfterOrUndefined; /** The page number or "undefined" if there is no page number to continue from */ diff --git a/x-pack/plugins/security_solution/public/common/images/illustration_product_no_results_magnifying_glass.svg b/x-pack/plugins/security_solution/public/common/images/illustration_product_no_results_magnifying_glass.svg new file mode 100644 index 0000000000000..b9a0df1630b20 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/images/illustration_product_no_results_magnifying_glass.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.tsx index c26b8cbc60d72..6658acab49856 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.tsx @@ -5,19 +5,20 @@ * 2.0. */ -import React from 'react'; -import type { EuiEmptyPromptProps } from '@elastic/eui'; +import React, { useMemo } from 'react'; import { + EuiLoadingContent, + EuiImage, EuiEmptyPrompt, EuiButton, useEuiTheme, - EuiPageTemplate, - EuiLoadingLogo, + EuiPanel, } from '@elastic/eui'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import * as i18n from './translations'; import type { ViewerState } from './reducer'; +import illustration from '../../../../common/images/illustration_product_no_results_magnifying_glass.svg'; interface ExeptionItemsViewerEmptyPromptsComponentProps { listType: ExceptionListTypeEnum; @@ -32,78 +33,84 @@ const ExeptionItemsViewerEmptyPromptsComponent = ({ }: ExeptionItemsViewerEmptyPromptsComponentProps): JSX.Element => { const { euiTheme } = useEuiTheme(); - let emptyPromptProps: Partial; - switch (currentState) { - case 'error': - emptyPromptProps = { - color: 'danger', - iconType: 'alert', - title:

{i18n.EXCEPTION_ERROR_TITLE}

, - body:

{i18n.EXCEPTION_ERROR_DESCRIPTION}

, - 'data-test-subj': 'exceptionItemViewerEmptyPrompts-error', - }; - break; - case 'empty': - emptyPromptProps = { - color: 'subdued', - iconType: 'plusInCircle', - iconColor: euiTheme.colors.darkestShade, - title: ( -

{i18n.EXCEPTION_EMPTY_PROMPT_TITLE}

- ), - body: ( -

- {listType === ExceptionListTypeEnum.ENDPOINT - ? i18n.EXCEPTION_EMPTY_ENDPOINT_PROMPT_BODY - : i18n.EXCEPTION_EMPTY_PROMPT_BODY} -

- ), - actions: [ - - {listType === ExceptionListTypeEnum.ENDPOINT - ? i18n.EXCEPTION_EMPTY_PROMPT_ENDPOINT_BUTTON - : i18n.EXCEPTION_EMPTY_PROMPT_BUTTON} - , - ], - 'data-test-subj': `exceptionItemViewerEmptyPrompts-empty-${listType}`, - }; - break; - case 'empty_search': - emptyPromptProps = { - color: 'subdued', - title: ( -

- {i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_TITLE} -

- ), - body: ( -

- {i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY} -

- ), - 'data-test-subj': 'exceptionItemViewerEmptyPrompts-emptySearch', - }; - break; - default: - emptyPromptProps = { - color: 'subdued', - icon: , - title:

{i18n.EXCEPTION_LOADING_TITLE}

, - 'data-test-subj': 'exceptionItemViewerEmptyPrompts-loading', - }; - break; - } + const content = useMemo(() => { + switch (currentState) { + case 'error': + return ( + {i18n.EXCEPTION_ERROR_TITLE}} + body={

{i18n.EXCEPTION_ERROR_DESCRIPTION}

} + data-test-subj={'exceptionItemViewerEmptyPrompts-error'} + /> + ); + case 'empty': + return ( + + {i18n.EXCEPTION_EMPTY_PROMPT_TITLE} + + } + body={ +

+ {listType === ExceptionListTypeEnum.ENDPOINT + ? i18n.EXCEPTION_EMPTY_ENDPOINT_PROMPT_BODY + : i18n.EXCEPTION_EMPTY_PROMPT_BODY} +

+ } + actions={[ + + {listType === ExceptionListTypeEnum.ENDPOINT + ? i18n.EXCEPTION_EMPTY_PROMPT_ENDPOINT_BUTTON + : i18n.EXCEPTION_EMPTY_PROMPT_BUTTON} + , + ]} + data-test-subj={`exceptionItemViewerEmptyPrompts-empty-${listType}`} + /> + ); + case 'empty_search': + return ( + } + title={

{i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_TITLE}

} + body={

{i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY}

} + data-test-subj="exceptionItemViewerEmptyPrompts-emptySearch" + /> + ); + default: + return ( + + ); + } + }, [currentState, euiTheme.colors.darkestShade, listType, onCreateExceptionListItem]); return ( - - - + + {content} + ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx index fd875c1b4f959..69a6b2d43250d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx @@ -37,7 +37,14 @@ import * as i18n from './translations'; import { useFindExceptionListReferences } from '../../logic/use_find_references'; import type { Rule } from '../../../../detections/containers/detection_engine/rules/types'; -const STATES_SEARCH_HIDDEN: ViewerState[] = ['error', 'empty', 'loading']; +const STATES_SEARCH_HIDDEN: ViewerState[] = ['error', 'empty']; +const STATES_PAGINATION_UTILITY_HIDDEN: ViewerState[] = [ + 'loading', + 'empty_search', + 'empty', + 'error', + 'searching', +]; const initialState: State = { pagination: { @@ -137,7 +144,7 @@ const ExceptionsViewerComponent = ({ page: pagination.pageIndex + 1, perPage: pagination.pageSize, }; - + console.log({exceptionLists}) const { page: pageIndex, per_page: itemsPerPage, @@ -299,9 +306,10 @@ const ExceptionsViewerComponent = ({ useEffect(() => { if (exceptionLists.length > 0) { + console.log('LENGTH', exceptionLists.length > 0) handleGetExceptionListItems(); } else { - setViewerState('loading'); + setViewerState('empty'); } }, [exceptionLists, handleGetExceptionListItems, setViewerState]); @@ -339,14 +347,16 @@ const ExceptionsViewerComponent = ({ <> {!STATES_SEARCH_HIDDEN.includes(viewerState) && ( + + )} + {!STATES_PAGINATION_UTILITY_HIDDEN.includes(viewerState) && ( <> - - @@ -364,7 +374,7 @@ const ExceptionsViewerComponent = ({ onCreateExceptionListItem={handleAddException} /> - {!STATES_SEARCH_HIDDEN.includes(viewerState) && ( + {!STATES_PAGINATION_UTILITY_HIDDEN.includes(viewerState) && ( { listType={ExceptionListTypeEnum.DETECTION} onSearch={jest.fn()} onAddExceptionClick={jest.fn()} + isSearching={false} isReadOnly /> ); @@ -32,6 +33,7 @@ describe('ExceptionsViewerSearchBar', () => { @@ -51,6 +53,7 @@ describe('ExceptionsViewerSearchBar', () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx index a516e0997e125..9c0651c8979c8 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx @@ -46,6 +46,7 @@ interface ExceptionsViewerSearchBarProps { // Exception list type used to determine what type of item is // being created when "onAddExceptionClick" is invoked listType: ExceptionListTypeEnum; + isSearching: boolean; onSearch: (arg: string) => void; onAddExceptionClick: (type: ExceptionListTypeEnum) => void; } @@ -56,6 +57,7 @@ interface ExceptionsViewerSearchBarProps { const ExceptionsViewerSearchBarComponent = ({ isReadOnly, listType, + isSearching, onSearch, onAddExceptionClick, }: ExceptionsViewerSearchBarProps): JSX.Element => { @@ -94,6 +96,7 @@ const ExceptionsViewerSearchBarComponent = ({ {addExceptionButtonText} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts index 3b04d7cc168d9..1fd3de325b7d0 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts @@ -10,14 +10,14 @@ import { i18n } from '@kbn/i18n'; export const EXCEPTION_NO_SEARCH_RESULTS_PROMPT_TITLE = i18n.translate( 'xpack.securitySolution.exceptions.allItems.noSearchResultsPromptTitle', { - defaultMessage: 'No results', + defaultMessage: 'No results match your search criteria', } ); export const EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY = i18n.translate( 'xpack.securitySolution.exceptions.allItems.noSearchResultsPromptBody', { - defaultMessage: 'No matching exception items were found in your search.', + defaultMessage: 'Try modifying your search.', } ); @@ -57,13 +57,6 @@ export const EXCEPTION_EMPTY_PROMPT_ENDPOINT_BUTTON = i18n.translate( } ); -export const EXCEPTION_LOADING_TITLE = i18n.translate( - 'xpack.securitySolution.exceptions.allItems.loadingExceptionsTitle', - { - defaultMessage: 'Loading exceptions', - } -); - export const EXCEPTION_ERROR_TITLE = i18n.translate( 'xpack.securitySolution.exceptions.allItems.exceptionItemsFetchError', { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index c07ab781b9e77..ce8f786e1b7f4 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -144,7 +144,7 @@ const StyledMinHeightTabContainer = styled.div` export enum RuleDetailTabs { alerts = 'alerts', - exceptions = 'exceptions', + exceptions = 'ruleExceptions', endpointExceptions = 'endpointExceptions', executionResults = 'executionResults', executionEvents = 'executionEvents', From 83545876b989b308337a95745f6b7feb2a1f5306 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 30 Aug 2022 08:53:25 -0700 Subject: [PATCH 19/30] updated per PR feedback --- .../integration/urls/not_found.spec.ts | 2 +- .../cypress/screens/rule_details.ts | 2 +- .../rules/all/use_columns.tsx | 21 +-- .../detection_engine/rules/details/index.tsx | 146 +++++++++--------- .../security_solution/public/rules/routes.tsx | 41 +++-- 5 files changed, 105 insertions(+), 107 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/urls/not_found.spec.ts b/x-pack/plugins/security_solution/cypress/integration/urls/not_found.spec.ts index ee4dda2e2181b..8bd3e6a9ed93b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/urls/not_found.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/urls/not_found.spec.ts @@ -51,7 +51,7 @@ describe('Display not found page', () => { }); it('navigates to the rules details page with incorrect link', () => { - visit(`${ruleDetailsUrl(mockRuleId)}/test/randomUrl`); + visit(`${ruleDetailsUrl(mockRuleId)}/randomUrl`); cy.get(NOT_FOUND).should('exist'); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index 1fb9df6508917..989353cf7a253 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -31,7 +31,7 @@ export const DETAILS_DESCRIPTION = '.euiDescriptionList__description'; export const DETAILS_TITLE = '.euiDescriptionList__title'; -export const EXCEPTIONS_TAB = '[data-test-subj="navigation-ruleExceptions"]'; +export const EXCEPTIONS_TAB = '[data-test-subj="navigation-rule_exceptions"]'; export const FALSE_POSITIVES_DETAILS = 'False positive examples'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx index 30706752eeffd..74caf92e5c31f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx @@ -11,7 +11,6 @@ import { FormattedMessage } from '@kbn/i18n-react'; import React, { useMemo } from 'react'; import { IntegrationsPopover } from '../../../../components/rules/related_integrations/integrations_popover'; import { - APP_UI_ID, DEFAULT_RELATIVE_DATE_THRESHOLD, SecurityPageName, SHOW_RELATED_INTEGRATIONS_SETTING, @@ -19,8 +18,6 @@ import { import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { getEmptyTagValue } from '../../../../../common/components/empty_value'; import { FormattedRelativePreferenceDate } from '../../../../../common/components/formatted_date'; -import { LinkAnchor } from '../../../../../common/components/links'; -import { useFormatUrl } from '../../../../../common/components/link_to'; import { getRuleDetailsUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { PopoverItems } from '../../../../../common/components/popover_items'; import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana'; @@ -44,6 +41,7 @@ import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; import { useStartTransaction } from '../../../../../common/lib/apm/use_start_transaction'; import { useInvalidateRules } from '../../../../containers/detection_engine/rules/use_find_rules_query'; import { useInvalidatePrePackagedRulesStatus } from '../../../../containers/detection_engine/rules/use_pre_packaged_rules_status'; +import { SecuritySolutionLinkAnchor } from '../../../../../common/components/links'; export type TableColumn = EuiBasicTableColumn | EuiTableActionsColumnType; @@ -90,24 +88,15 @@ const useEnabledColumn = ({ hasPermissions }: ColumnsProps): TableColumn => { }; export const RuleLink = ({ name, id }: Pick) => { - const { formatUrl } = useFormatUrl(SecurityPageName.rules); - const { navigateToApp } = useKibana().services.application; - return ( - void }) => { - ev.preventDefault(); - navigateToApp(APP_UI_ID, { - deepLinkId: SecurityPageName.rules, - path: getRuleDetailsUrl(id), - }); - }} - href={formatUrl(getRuleDetailsUrl(id))} + deepLinkId={SecurityPageName.rules} + path={getRuleDetailsUrl(id)} > {name} - + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 985f4007ff8f8..b8eb42292f772 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -18,11 +18,11 @@ import { EuiWindowEvent, } from '@elastic/eui'; import { i18n as i18nTranslate } from '@kbn/i18n'; - +import { Route } from '@kbn/kibana-react-plugin/public'; import { FormattedMessage } from '@kbn/i18n-react'; import { noop, omit } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import { Switch, useParams } from 'react-router-dom'; import type { ConnectedProps } from 'react-redux'; import { connect, useDispatch } from 'react-redux'; import styled from 'styled-components'; @@ -146,9 +146,9 @@ const StyledMinHeightTabContainer = styled.div` export enum RuleDetailTabs { alerts = 'alerts', - exceptions = 'ruleExceptions', - executionResults = 'executionResults', - executionEvents = 'executionEvents', + exceptions = 'rule_exceptions', + executionResults = 'execution_results', + executionEvents = 'execution_events', } export const RULE_DETAILS_TAB_NAME: Record = { @@ -804,74 +804,76 @@ const RuleDetailsPageComponent: React.FC = ({ - {pageTabName === RuleDetailTabs.alerts && hasIndexRead && ( - <> - - - + + <> + + + + + + {updatedAt && + timelinesUi.getLastUpdated({ + updatedAt: updatedAt || Date.now(), + showUpdating, + })} + + + + + - - - {updatedAt && - timelinesUi.getLastUpdated({ - updatedAt: updatedAt || Date.now(), - showUpdating, - })} - - - - - - - - {ruleId != null && ( - - )} - - )} - {pageTabName === RuleDetailTabs.exceptions && ( - - )} - {pageTabName === RuleDetailTabs.executionResults && ( - - )} - {pageTabName === RuleDetailTabs.executionEvents && ( - - )} + + + {ruleId != null && ( + + )} + + + + + + + + + + + + diff --git a/x-pack/plugins/security_solution/public/rules/routes.tsx b/x-pack/plugins/security_solution/public/rules/routes.tsx index 13adb118030d4..3b1f8ba8962e2 100644 --- a/x-pack/plugins/security_solution/public/rules/routes.tsx +++ b/x-pack/plugins/security_solution/public/rules/routes.tsx @@ -14,7 +14,10 @@ import { RULES_PATH, SecurityPageName } from '../../common/constants'; import { NotFoundPage } from '../app/404'; import { RulesPage } from '../detections/pages/detection_engine/rules'; import { CreateRulePage } from '../detections/pages/detection_engine/rules/create'; -import { RuleDetailsPage } from '../detections/pages/detection_engine/rules/details'; +import { + RuleDetailsPage, + RuleDetailTabs, +} from '../detections/pages/detection_engine/rules/details'; import { EditRulePage } from '../detections/pages/detection_engine/rules/edit'; import { useReadonlyHeader } from '../use_readonly_header'; import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper'; @@ -22,20 +25,21 @@ import { SpyRoute } from '../common/utils/route/spy_routes'; const RulesSubRoutes = [ { - path: '/rules/id/:detailName/edit', - main: EditRulePage, - exact: true, - }, - { - path: '/rules/id/:detailName', + path: `/rules/id/:detailName/:tabName(${RuleDetailTabs.alerts}|${RuleDetailTabs.exceptions}|${RuleDetailTabs.executionResults}|${RuleDetailTabs.executionEvents})`, main: RuleDetailsPage, exact: true, }, { - path: '/rules/id/:detailName/:tabName', - main: RuleDetailsPage, + path: '/rules/id/:detailName/edit', + main: EditRulePage, exact: true, }, + + // { + // path: '/rules/id/:detailName', + // main: RuleDetailsPage, + // exact: true, + // }, { path: '/rules/create', main: CreateRulePage, @@ -55,22 +59,25 @@ const RulesContainerComponent: React.FC = () => { - {/* Because '/rules/id/:detailName/edit' would match '/rules/id/:detailName/:tabName' need - to add a redirect to ensure that it continues to work as expected */} - ( + render={({ + match: { + params: { detailName }, + }, + location, + }) => ( )} /> - {RulesSubRoutes.map((route, index) => ( + {RulesSubRoutes.map((route) => ( Date: Tue, 30 Aug 2022 09:13:10 -0700 Subject: [PATCH 20/30] cleanup --- .../detection_engine/rules/all/use_columns.tsx | 5 +++-- .../security_solution/public/rules/routes.tsx | 14 ++++---------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx index 74caf92e5c31f..3b413cf67e4d5 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx @@ -18,7 +18,7 @@ import { import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { getEmptyTagValue } from '../../../../../common/components/empty_value'; import { FormattedRelativePreferenceDate } from '../../../../../common/components/formatted_date'; -import { getRuleDetailsUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine'; +import { getRuleDetailsTabUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { PopoverItems } from '../../../../../common/components/popover_items'; import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana'; import { canEditRuleWithActions, getToolTipContent } from '../../../../../common/utils/privileges'; @@ -42,6 +42,7 @@ import { useStartTransaction } from '../../../../../common/lib/apm/use_start_tra import { useInvalidateRules } from '../../../../containers/detection_engine/rules/use_find_rules_query'; import { useInvalidatePrePackagedRulesStatus } from '../../../../containers/detection_engine/rules/use_pre_packaged_rules_status'; import { SecuritySolutionLinkAnchor } from '../../../../../common/components/links'; +import { RuleDetailTabs } from '../details'; export type TableColumn = EuiBasicTableColumn | EuiTableActionsColumnType; @@ -93,7 +94,7 @@ export const RuleLink = ({ name, id }: Pick) => { {name} diff --git a/x-pack/plugins/security_solution/public/rules/routes.tsx b/x-pack/plugins/security_solution/public/rules/routes.tsx index 3b1f8ba8962e2..0ca80c735344f 100644 --- a/x-pack/plugins/security_solution/public/rules/routes.tsx +++ b/x-pack/plugins/security_solution/public/rules/routes.tsx @@ -25,21 +25,15 @@ import { SpyRoute } from '../common/utils/route/spy_routes'; const RulesSubRoutes = [ { - path: `/rules/id/:detailName/:tabName(${RuleDetailTabs.alerts}|${RuleDetailTabs.exceptions}|${RuleDetailTabs.executionResults}|${RuleDetailTabs.executionEvents})`, - main: RuleDetailsPage, + path: '/rules/id/:detailName/edit', + main: EditRulePage, exact: true, }, { - path: '/rules/id/:detailName/edit', - main: EditRulePage, + path: `/rules/id/:detailName/:tabName(${RuleDetailTabs.alerts}|${RuleDetailTabs.exceptions}|${RuleDetailTabs.executionResults}|${RuleDetailTabs.executionEvents})`, + main: RuleDetailsPage, exact: true, }, - - // { - // path: '/rules/id/:detailName', - // main: RuleDetailsPage, - // exact: true, - // }, { path: '/rules/create', main: CreateRulePage, From 1ecd133e44bae041a52de5d0be5e563907062905 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 30 Aug 2022 09:22:03 -0700 Subject: [PATCH 21/30] fix type errors --- .../rules/details/__mocks__/rule_details_context.tsx | 3 ++- .../detections/pages/detection_engine/rules/details/index.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/__mocks__/rule_details_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/__mocks__/rule_details_context.tsx index e8cdcebbc938a..f4b04627b5164 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/__mocks__/rule_details_context.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/__mocks__/rule_details_context.tsx @@ -7,10 +7,11 @@ import React from 'react'; import type { RuleDetailsContextType } from '../rule_details_context'; +import { RuleDetailTabs } from '..'; export const useRuleDetailsContextMock = { create: (): jest.Mocked => ({ - executionResults: { + [RuleDetailTabs.executionResults]: { state: { superDatePicker: { recentlyUsedRanges: [], diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index b8eb42292f772..fedeb9a1e9c7d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -221,7 +221,7 @@ const RuleDetailsPageComponent: React.FC = ({ } = useSourcererDataView(SourcererScopeName.detections); const loading = userInfoLoading || listsConfigLoading; - const { detailName: ruleId, tabName: pageTabName } = useParams<{ + const { detailName: ruleId } = useParams<{ detailName: string; tabName: string; }>(); From 4f050a07438fd7786ff14feef8ae2af6b1c9dd1a Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 30 Aug 2022 13:31:07 -0700 Subject: [PATCH 22/30] updating per PR feedback --- .../pages/detection_engine/rules/details/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index fedeb9a1e9c7d..4dd6f6c9e927c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -805,7 +805,7 @@ const RuleDetailsPageComponent: React.FC = ({ - + <> @@ -855,7 +855,7 @@ const RuleDetailsPageComponent: React.FC = ({ )} - + = ({ onRuleChange={refreshRule} /> - + - + From aa8dba22431b7ba4a35ce59b0279275e1dff7a41 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 30 Aug 2022 15:02:38 -0700 Subject: [PATCH 23/30] updating to account for some new changes around tab routing in rule details --- .../kbn-securitysolution-list-hooks/index.ts | 1 - .../all_exception_items_table/index.tsx | 45 ++++++++++++------- .../all_exception_items_table/reducer.ts | 10 +++++ .../all_exception_items_table/translations.ts | 13 ++++++ .../components/exception_item_card/index.tsx | 3 +- .../rule_exceptions/utils/types.ts | 1 + .../detection_engine/rules/details/index.tsx | 16 +++++-- .../security_solution/public/rules/routes.tsx | 2 +- .../rules/find_rule_exceptions_route.ts | 11 +++-- 9 files changed, 74 insertions(+), 28 deletions(-) diff --git a/packages/kbn-securitysolution-list-hooks/index.ts b/packages/kbn-securitysolution-list-hooks/index.ts index 4c523fe577211..c2469bc4c4948 100644 --- a/packages/kbn-securitysolution-list-hooks/index.ts +++ b/packages/kbn-securitysolution-list-hooks/index.ts @@ -10,7 +10,6 @@ export * from './src/use_api'; export * from './src/use_create_list_index'; export * from './src/use_cursor'; export * from './src/use_delete_list'; -export * from './src/use_exception_list_items'; export * from './src/use_exception_lists'; export * from './src/use_export_list'; export * from './src/use_find_lists'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx index 69a6b2d43250d..49af0d2f9a097 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx @@ -61,7 +61,7 @@ const initialState: State = { }; interface ExceptionsViewerProps { - rule: Rule; + rule: Rule | null; listType: ExceptionListTypeEnum; onRuleChange?: () => void; } @@ -83,10 +83,6 @@ const ExceptionsViewerComponent = ({ dispatch, ] = useReducer(allExceptionItemsReducer(), { ...initialState, - exceptionLists: - rule != null && rule.exceptions_list != null - ? rule.exceptions_list.filter((list) => list.type === listType) - : [], }); // Reducer actions @@ -116,6 +112,16 @@ const ExceptionsViewerComponent = ({ [dispatch] ); + const setExceptionLists = useCallback( + (lists: ListArray): void => { + dispatch({ + type: 'setExceptionLists', + lists, + }); + }, + [dispatch] + ); + const setFlyoutType = useCallback( (flyoutType: ViewerFlyoutName): void => { dispatch({ @@ -126,9 +132,7 @@ const ExceptionsViewerComponent = ({ [dispatch] ); - const [_, allReferences] = useFindExceptionListReferences( - rule != null ? rule.exceptions_list ?? [] : [] - ); + const [_, allReferences] = useFindExceptionListReferences(exceptionLists); const handleFetchItems = useCallback( async (options?: { pagination?: Partial; search?: string; filters?: string }) => { @@ -144,7 +148,6 @@ const ExceptionsViewerComponent = ({ page: pagination.pageIndex + 1, perPage: pagination.pageSize, }; - console.log({exceptionLists}) const { page: pageIndex, per_page: itemsPerPage, @@ -268,7 +271,7 @@ const ExceptionsViewerComponent = ({ }, [setFlyoutType, handleGetExceptionListItems]); const handleDeleteException = useCallback( - async ({ id: itemId, namespaceType }: ExceptionListItemIdentifiers) => { + async ({ id: itemId, name, namespaceType }: ExceptionListItemIdentifiers) => { const abortCtrl = new AbortController(); try { @@ -281,9 +284,12 @@ const ExceptionsViewerComponent = ({ signal: abortCtrl.signal, }); - await handleGetExceptionListItems(); + toasts.addSuccess({ + title: i18n.EXCEPTION_ITEM_DELETE_TITLE, + text: i18n.EXCEPTION_ITEM_DELETE_TEXT(name), + }); - setViewerState(null); + await handleGetExceptionListItems(); } catch (e) { setViewerState('error'); @@ -304,12 +310,21 @@ const ExceptionsViewerComponent = ({ } }, [setReadOnly, canUserCRUD, hasIndexWrite]); + useEffect(() => { + if (rule != null) { + const lists = + rule != null && rule.exceptions_list != null + ? rule.exceptions_list.filter((list) => list.type === listType) + : []; + setExceptionLists(lists); + } else { + setViewerState('loading'); + } + }, [listType, rule, setViewerState, setExceptionLists]); + useEffect(() => { if (exceptionLists.length > 0) { - console.log('LENGTH', exceptionLists.length > 0) handleGetExceptionListItems(); - } else { - setViewerState('empty'); } }, [exceptionLists, handleGetExceptionListItems, setViewerState]); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/reducer.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/reducer.ts index 5a7d1e312c44d..a93f473f3c0b5 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/reducer.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/reducer.ts @@ -41,6 +41,10 @@ export type Action = exceptions: ExceptionListItemSchema[]; pagination: Pagination; } + | { + type: 'setExceptionLists'; + lists: ListArray; + } | { type: 'updateFlyoutOpen'; flyoutType: ViewerFlyoutName } | { type: 'updateExceptionToEdit'; @@ -69,6 +73,12 @@ export const allExceptionItemsReducer = exceptions, }; } + case 'setExceptionLists': { + return { + ...state, + exceptionLists: action.lists, + }; + } case 'updateExceptionToEdit': { const { exception } = action; return { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts index 1fd3de325b7d0..727fb78bede2a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts @@ -99,3 +99,16 @@ export const EXCEPTION_ITEMS_PAGINATION_ARIA_LABEL = i18n.translate( defaultMessage: 'Exception item table pagination', } ); + +export const EXCEPTION_ITEM_DELETE_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.exceptionItemDeleteSuccessTitle', + { + defaultMessage: 'Exception deleted', + } +); + +export const EXCEPTION_ITEM_DELETE_TEXT = (itemName: string) => + i18n.translate('xpack.securitySolution.exceptions.allItems.exceptionItemDeleteSuccessText', { + values: { itemName }, + defaultMessage: '"{itemName}" deleted successfully.', + }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.tsx index 41df5d089da96..cae3136dc6ad4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.tsx @@ -42,9 +42,10 @@ const ExceptionItemCardComponent = ({ const handleDelete = useCallback((): void => { onDeleteException({ id: exceptionItem.id, + name: exceptionItem.name, namespaceType: exceptionItem.namespace_type, }); - }, [onDeleteException, exceptionItem.id, exceptionItem.namespace_type]); + }, [onDeleteException, exceptionItem.id, exceptionItem.name, exceptionItem.namespace_type]); const handleEdit = useCallback((): void => { onEditException(exceptionItem); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/types.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/types.ts index 89b7783f58ec2..56db2ad8257f8 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/types.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/types.ts @@ -11,6 +11,7 @@ import type { CodeSignature } from '../../../../common/ecs/file'; export interface ExceptionListItemIdentifiers { id: string; + name: string; namespaceType: NamespaceType; } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index f22a70b75f0ed..03412cb19d1f2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -246,13 +246,13 @@ const RuleDetailsPageComponent: React.FC = ({ [RuleDetailTabs.exceptions]: { id: RuleDetailTabs.exceptions, name: RULE_DETAILS_TAB_NAME[RuleDetailTabs.exceptions], - disabled: false, + disabled: rule == null, href: `/rules/id/${ruleId}/${RuleDetailTabs.exceptions}`, }, [RuleDetailTabs.endpointExceptions]: { id: RuleDetailTabs.endpointExceptions, name: RULE_DETAILS_TAB_NAME[RuleDetailTabs.endpointExceptions], - disabled: false, + disabled: rule == null, href: `/rules/id/${ruleId}/${RuleDetailTabs.endpointExceptions}`, }, [RuleDetailTabs.executionResults]: { @@ -268,7 +268,7 @@ const RuleDetailsPageComponent: React.FC = ({ href: `/rules/id/${ruleId}/${RuleDetailTabs.executionEvents}`, }, }), - [isExistingRule, ruleId] + [isExistingRule, rule, ruleId] ); const [pageTabs, setTabs] = useState>>(ruleDetailTabs); @@ -395,11 +395,19 @@ const RuleDetailsPageComponent: React.FC = ({ if (!ruleExecutionSettings.extendedLogging.isEnabled) { hiddenTabs.push(RuleDetailTabs.executionEvents); } + if (rule != null) { + const hasEndpointList = (rule.exceptions_list ?? []).some( + (list) => list.type === ExceptionListTypeEnum.ENDPOINT + ); + if (!hasEndpointList) { + hiddenTabs.push(RuleDetailTabs.endpointExceptions); + } + } const tabs = omit>(hiddenTabs, ruleDetailTabs); setTabs(tabs); - }, [hasIndexRead, ruleDetailTabs, ruleExecutionSettings]); + }, [hasIndexRead, rule, ruleDetailTabs, ruleExecutionSettings]); const showUpdating = useMemo( () => isLoadingIndexPattern || isAlertsLoading || loading, diff --git a/x-pack/plugins/security_solution/public/rules/routes.tsx b/x-pack/plugins/security_solution/public/rules/routes.tsx index 0ca80c735344f..633b4005110af 100644 --- a/x-pack/plugins/security_solution/public/rules/routes.tsx +++ b/x-pack/plugins/security_solution/public/rules/routes.tsx @@ -30,7 +30,7 @@ const RulesSubRoutes = [ exact: true, }, { - path: `/rules/id/:detailName/:tabName(${RuleDetailTabs.alerts}|${RuleDetailTabs.exceptions}|${RuleDetailTabs.executionResults}|${RuleDetailTabs.executionEvents})`, + path: `/rules/id/:detailName/:tabName(${RuleDetailTabs.alerts}|${RuleDetailTabs.exceptions}|${RuleDetailTabs.endpointExceptions}|${RuleDetailTabs.executionResults}|${RuleDetailTabs.executionEvents})`, main: RuleDetailsPage, exact: true, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts index 17986310e774e..e0e9caf4d0ba8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts @@ -46,7 +46,7 @@ export const findRuleExceptionReferencesRoute = (router: SecuritySolutionPluginR if (ids.length !== namespaceTypes.length || ids.length !== listIds.length) { return siemResponse.error({ - body: `"ids", "list_ids" and "namespace_types" need to have the same comma separated number of values. Expected "ids" length: ${ids.length} to equal "namespace_types" length: ${namespaceTypes.length} and "list_ids" legnth: ${listIds.length}.`, + body: `"ids", "list_ids" and "namespace_types" need to have the same comma separated number of values. Expected "ids" length: ${ids.length} to equal "namespace_types" length: ${namespaceTypes.length} and "list_ids" length: ${listIds.length}.`, statusCode: 400, }); } @@ -55,7 +55,7 @@ export const findRuleExceptionReferencesRoute = (router: SecuritySolutionPluginR ids.map(async (id, index) => { return rulesClient.find({ options: { - perPage: 1000, + perPage: 10000, filter: enrichFilterWithRuleTypeMapping(null), hasReference: { id, @@ -66,16 +66,15 @@ export const findRuleExceptionReferencesRoute = (router: SecuritySolutionPluginR }) ); - const references = results.reduce((acc, { data }, index) => { + const references = results.map(({ data }, index) => { const wantedData = data.map(({ name, id, params }) => ({ name, id, rule_id: params.ruleId, exception_lists: params.exceptionsList, })); - - return [...acc, { [listIds[index]]: wantedData }]; - }, []); + return { [listIds[index]]: wantedData }; + }); const [validated, errors] = validate({ references }, rulesReferencedByExceptionListsSchema); From fd0d6dabadf8b1128a35403a7250c49a88544c6e Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 30 Aug 2022 22:27:54 -0700 Subject: [PATCH 24/30] fixing some cypress issues --- 4688.txt | 307 ------------------ .../src/api/index.ts | 1 + .../exception_lists/exception_list_client.ts | 3 + .../exception_list_client_types.ts | 2 + .../exceptions/exceptions_flyout.spec.ts | 10 +- .../cypress/screens/exceptions.ts | 3 +- .../cypress/tasks/rule_details.ts | 11 +- .../all_exception_items_table/index.tsx | 100 +++--- .../all_exception_items_table/pagination.tsx | 15 +- .../all_exception_items_table/reducer.ts | 18 +- .../all_exception_items_table/search_bar.tsx | 3 +- .../exception_item_card/index.test.tsx | 1 + .../rules/details/index.test.tsx | 12 +- .../rules/find_rule_exceptions_route.test.ts | 4 +- 14 files changed, 88 insertions(+), 402 deletions(-) delete mode 100644 4688.txt diff --git a/4688.txt b/4688.txt deleted file mode 100644 index 33eca94bd7c2d..0000000000000 --- a/4688.txt +++ /dev/null @@ -1,307 +0,0 @@ - -{ - "_index": ".ds-logs-windows_custom_domain-default-2022.08.04-000018", - "_id": "ZgknxoIBrnX8mZ5R-j-7", - "_version": 1, - "_score": 0, - "_source": { - "agent": { - "name": "WEC-Server", - "id": "69566376-69e2-4630-b061-faeb1094d11e", - "ephemeral_id": "bd6a5bfb-5763-4026-85b8-94d037c993fd", - "type": "filebeat", - "version": "8.3.3" - }, - "winlog": { - "computer_name": "ServerEventCreate.domain", - "process": { - "pid": 4, - "thread": { - "id": 16536 - } - }, - "keywords": [ - "Audit Success" - ], - "channel": "Security", - "event_data": { - "MandatoryLabel": "S-1-16-8192", - "ParentProcessName": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", - "NewProcessId": "0x2eb0", - "TokenElevationType": "%%1936", - "SubjectLogonId": "0x134ad751", - "NewProcessName": "C:\\Windows\\System32\\whoami.exe", - "TargetLogonId": "0x0", - "SubjectUserName": "User.name", - "CommandLine": "\"C:\\Windows\\system32\\whoami.exe\" /priv", - "SubjectDomainName": "Domain", - "TargetUserName": "-", - "ProcessId": "0x5e88", - "TargetDomainName": "-", - "SubjectUserSid": "S-1-5-21-321992054-2886514165-123123123-1160", - "TargetUserSid": "S-1-0-0" - }, - "opcode": "Info", - "version": 2, - "record_id": 10111571, - "event_id": "4688", - "task": "Process Creation", - "provider_guid": "{54849625-5478-4994-a5ba-3e3b0328c30d}", - "api": "wineventlog", - "provider_name": "Microsoft-Windows-Security-Auditing" - }, - "log": { - "level": "information" - }, - "elastic_agent": { - "id": "69566376-69e2-4630-b061-faeb1094d11e", - "version": "8.3.3", - "snapshot": false - }, - "message": "A new process has been created.\n\nCreator Subject:\n\tSecurity ID:\t\tS-1-5-21-321992054-2886514165-175804892-1160\n\tAccount Name:\t\tuser.name\n\tAccount Domain:\t\tdomain\n\tLogon ID:\t\t0x134AD751\n\nTarget Subject:\n\tSecurity ID:\t\tS-1-0-0\n\tAccount Name:\t\t-\n\tAccount Domain:\t\t-\n\tLogon ID:\t\t0x0\n\nProcess Information:\n\tNew Process ID:\t\t0x2eb0\n\tNew Process Name:\tC:\\Windows\\System32\\whoami.exe\n\tToken Elevation Type:\t%%1936\n\tMandatory Label:\t\tS-1-16-8192\n\tCreator Process ID:\t0x5e88\n\tCreator Process Name:\tC:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\n\tProcess Command Line:\t\"C:\\Windows\\system32\\whoami.exe\" /priv\n\nToken Elevation Type indicates the type of token that was assigned to the new process in accordance with User Account Control policy.\n\nType 1 is a full token with no privileges removed or groups disabled. A full token is only used if User Account Control is disabled or if the user is the built-in Administrator account or a service account.\n\nType 2 is an elevated token with no privileges removed or groups disabled. An elevated token is used when User Account Control is enabled and the user chooses to start the program using Run as administrator. An elevated token is also used when an application is configured to always require administrative privilege or to always require maximum privilege, and the user is a member of the Administrators group.\n\nType 3 is a limited token with administrative privileges removed and administrative groups disabled. The limited token is used when User Account Control is enabled, the application does not require administrative privilege, and the user does not choose to start the program using Run as administrator.", - "input": { - "type": "winlog" - }, - "@timestamp": "2022-08-22T15:21:03.631Z", - "ecs": { - "version": "8.0.0" - }, - "parser": { - "version": "1.0" - }, - "data_stream": { - "namespace": "default", - "type": "logs", - "dataset": "windows_custom_domain" - }, - "host": { - "hostname": "Wec-Server", - "os": { - "build": "17763.3165", - "kernel": "10.0.17763.3165 (WinBuild.160101.0800)", - "name": "Windows Server 2019 Datacenter", - "type": "windows", - "family": "windows", - "version": "10.0", - "platform": "windows" - }, - "ip": [ - "fe80::b92d:654c:2151:8b7b", - "10.10.10.10" - ], - "name": "ServerEventCreated.domain", - "id": "1589e245-c480-4426-8867-2c13e9fc009e", - "mac": [ - "00:15:5d:f6:8a:0d" - ], - "architecture": "x86_64" - }, - "event": { - "code": "4688", - "provider": "Microsoft-Windows-Security-Auditing", - "created": "2022-08-22T15:24:43.970Z", - "kind": "event", - "action": "Process Creation", - "dataset": "windows_custom_domain", - "outcome": "success" - } - }, - "fields": { - "elastic_agent.version": [ - "8.3.3" - ], - "winlog.event_data.NewProcessName": [ - "C:\\Windows\\System32\\whoami.exe" - ], - "winlog.provider_guid": [ - "{54849625-5478-4994-a5ba-3e3b0328c30d}" - ], - "winlog.provider_name": [ - "Microsoft-Windows-Security-Auditing" - ], - "host.hostname": [ - "WEC-Server" - ], - "winlog.computer_name": [ - "ServerEventCreated.domain" - ], - "host.mac": [ - "00:15:5d:f6:8a:0d" - ], - "winlog.process.pid": [ - 4 - ], - "host.os.version": [ - "10.0" - ], - "winlog.keywords": [ - "Audit Success" - ], - "winlog.record_id": [ - 10111571 - ], - "winlog.event_data.CommandLine": [ - "\"C:\\Windows\\system32\\whoami.exe\" /priv" - ], - "host.os.name": [ - "Windows Server 2019 Datacenter" - ], - "log.level": [ - "information" - ], - "agent.name": [ - "WEC-Server" - ], - "winlog.event_data.ParentProcessName": [ - "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" - ], - "host.name": [ - "ServerEventCreated.domain" - ], - "event.kind": [ - "event" - ], - "winlog.event_data.NewProcessId": [ - "0x2eb0" - ], - "event.outcome": [ - "success" - ], - "winlog.version": [ - 2 - ], - "winlog.event_data.TargetUserName": [ - "-" - ], - "host.os.type": [ - "windows" - ], - "winlog.event_data.TargetUserSid": [ - "S-1-0-0" - ], - "input.type": [ - "winlog" - ], - "data_stream.type": [ - "logs" - ], - "host.architecture": [ - "x86_64" - ], - "event.provider": [ - "Microsoft-Windows-Security-Auditing" - ], - "event.code": [ - "4688" - ], - "agent.id": [ - "69566376-69e2-4630-b061-faeb1094d11e" - ], - "ecs.version": [ - "8.0.0" - ], - "winlog.event_data.TokenElevationType": [ - "%%1936" - ], - "event.created": [ - "2022-08-22T15:24:43.970Z" - ], - "agent.version": [ - "8.3.3" - ], - "host.os.family": [ - "windows" - ], - "winlog.event_data.SubjectUserSid": [ - "S-1-5-21-321992054-2886514165-17512312423-1160" - ], - "winlog.process.thread.id": [ - 16536 - ], - "host.os.build": [ - "17763.3165" - ], - "host.ip": [ - "fe80::b92d:654c:2151:8b7b", - "10.10.10.10" - ], - "agent.type": [ - "filebeat" - ], - "winlog.event_data.SubjectLogonId": [ - "0x134ad751" - ], - "winlog.event_data.TargetLogonId": [ - "0x0" - ], - "host.os.kernel": [ - "10.0.17763.3165 (WinBuild.160101.0800)" - ], - "winlog.api": [ - "wineventlog" - ], - "elastic_agent.snapshot": [ - false - ], - "winlog.event_data.ProcessId": [ - "0x5e88" - ], - "host.id": [ - "1589e245-c480-4426-8867-2c13e9fc009e" - ], - "winlog.task": [ - "Process Creation" - ], - "elastic_agent.id": [ - "69566376-69e2-4630-b061-faeb1094d11e" - ], - "winlog.event_data.SubjectUserName": [ - "user.name" - ], - "data_stream.namespace": [ - "default" - ], - "parser.version": [ - "1.0" - ], - "winlog.event_id": [ - "4688" - ], - "message": [ - "A new process has been created.\n\nCreator Subject:\n\tSecurity ID:\t\tS-1-5-21-321992054-2886514165-2131243324-1160\n\tAccount Name:\t\t1user.name\n\tAccount Domain:\t\tdomain\n\tLogon ID:\t\t0x134AD751\n\nTarget Subject:\n\tSecurity ID:\t\tS-1-0-0\n\tAccount Name:\t\t-\n\tAccount Domain:\t\t-\n\tLogon ID:\t\t0x0\n\nProcess Information:\n\tNew Process ID:\t\t0x2eb0\n\tNew Process Name:\tC:\\Windows\\System32\\whoami.exe\n\tToken Elevation Type:\t%%1936\n\tMandatory Label:\t\tS-1-16-8192\n\tCreator Process ID:\t0x5e88\n\tCreator Process Name:\tC:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\n\tProcess Command Line:\t\"C:\\Windows\\system32\\whoami.exe\" /priv\n\nToken Elevation Type indicates the type of token that was assigned to the new process in accordance with User Account Control policy.\n\nType 1 is a full token with no privileges removed or groups disabled. A full token is only used if User Account Control is disabled or if the user is the built-in Administrator account or a service account.\n\nType 2 is an elevated token with no privileges removed or groups disabled. An elevated token is used when User Account Control is enabled and the user chooses to start the program using Run as administrator. An elevated token is also used when an application is configured to always require administrative privilege or to always require maximum privilege, and the user is a member of the Administrators group.\n\nType 3 is a limited token with administrative privileges removed and administrative groups disabled. The limited token is used when User Account Control is enabled, the application does not require administrative privilege, and the user does not choose to start the program using Run as administrator." - ], - "event.action": [ - "Process Creation" - ], - "@timestamp": [ - "2022-08-22T15:21:03.631Z" - ], - "winlog.channel": [ - "Security" - ], - "host.os.platform": [ - "windows" - ], - "winlog.event_data.MandatoryLabel": [ - "S-1-16-8192" - ], - "winlog.event_data.TargetDomainName": [ - "-" - ], - "data_stream.dataset": [ - "windows_custom_domain" - ], - "winlog.opcode": [ - "Info" - ], - "agent.ephemeral_id": [ - "bd6a5bfb-5763-4026-85b8-94d037c993fd" - ], - "winlog.event_data.SubjectDomainName": [ - "domain" - ], - "event.dataset": [ - "windows_custom_domain" - ] - } -} \ No newline at end of file diff --git a/packages/kbn-securitysolution-list-api/src/api/index.ts b/packages/kbn-securitysolution-list-api/src/api/index.ts index e33d911f40cf5..871987ff7d367 100644 --- a/packages/kbn-securitysolution-list-api/src/api/index.ts +++ b/packages/kbn-securitysolution-list-api/src/api/index.ts @@ -322,6 +322,7 @@ export { fetchExceptionListByIdWithValidation as fetchExceptionListById }; * @param http Kibana http service * @param listIds ExceptionList list_ids (not ID) * @param namespaceTypes ExceptionList namespace_types + * @param search optional - simple search string * @param filter optional * @param pagination optional * @param signal to cancel request diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index 5f1f4c018586b..baa9d943127f7 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -904,6 +904,7 @@ export class ExceptionListClient { * @param options.perPage How many per page to return * @param options.page The page number or "undefined" if there is no page number to continue from * @param options.pit The Point in Time (pit) id if there is one, otherwise "undefined" can be sent in + * @param options.search The simple query search parameter if there is one, otherwise "undefined" can be sent in * @param options.searchAfter The search_after parameter if there is one, otherwise "undefined" can be sent in * @param options.sortField The sort field string if there is one, otherwise "undefined" can be sent in * @param options.sortOrder The sort order of "asc" or "desc", otherwise "undefined" can be sent in @@ -914,6 +915,7 @@ export class ExceptionListClient { perPage, page, pit, + search, searchAfter, sortField, sortOrder, @@ -928,6 +930,7 @@ export class ExceptionListClient { perPage, pit, savedObjectsClient, + search, searchAfter, sortField, sortOrder, diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index 2e4e63d9a1c18..048930e51b93d 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -385,6 +385,8 @@ export interface FindEndpointListItemOptions { perPage: PerPageOrUndefined; /** The Point in Time (pit) id if there is one, otherwise "undefined" can be sent in */ pit?: PitOrUndefined; + /** The simple search parameter if there is one, otherwise "undefined" can be sent in */ + search?: SearchOrUndefined; /** The search_after parameter if there is one, otherwise "undefined" can be sent in */ searchAfter?: SearchAfterOrUndefined; /** The page number or "undefined" if there is no page number to continue from */ diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts index 8c2e2af4b8bad..2b3f39150dd76 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts @@ -34,7 +34,7 @@ import { FIELD_INPUT, LOADING_SPINNER, EXCEPTION_ITEM_CONTAINER, - ADD_EXCEPTIONS_BTN, + ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN, EXCEPTION_FIELD_LIST, EXCEPTION_EDIT_FLYOUT_SAVE_BTN, EXCEPTION_FLYOUT_VERSION_CONFLICT, @@ -94,7 +94,7 @@ describe('Exceptions flyout', () => { it('Validates empty entry values correctly', () => { cy.root() .pipe(($el) => { - $el.find(ADD_EXCEPTIONS_BTN).trigger('click'); + $el.find(ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN).trigger('click'); return $el.find(ADD_AND_BTN); }) .should('be.visible'); @@ -123,7 +123,7 @@ describe('Exceptions flyout', () => { it('Does not overwrite values and-ed together', () => { cy.root() .pipe(($el) => { - $el.find(ADD_EXCEPTIONS_BTN).trigger('click'); + $el.find(ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN).trigger('click'); return $el.find(ADD_AND_BTN); }) .should('be.visible'); @@ -146,7 +146,7 @@ describe('Exceptions flyout', () => { it('Does not overwrite values or-ed together', () => { cy.root() .pipe(($el) => { - $el.find(ADD_EXCEPTIONS_BTN).trigger('click'); + $el.find(ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN).trigger('click'); return $el.find(ADD_AND_BTN); }) .should('be.visible'); @@ -267,7 +267,7 @@ describe('Exceptions flyout', () => { it('Contains custom index fields', () => { cy.root() .pipe(($el) => { - $el.find(ADD_EXCEPTIONS_BTN).trigger('click'); + $el.find(ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN).trigger('click'); return $el.find(ADD_AND_BTN); }) .should('be.visible'); diff --git a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts index a02a318bfb307..56dcfdbf97ead 100644 --- a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts @@ -5,7 +5,8 @@ * 2.0. */ -export const ADD_EXCEPTIONS_BTN = '[data-test-subj="exceptionsHeaderAddExceptionBtn"]'; +export const ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN = + '[data-test-subj="exceptionsEmptyPromptButton"]'; export const CLOSE_ALERTS_CHECKBOX = '[data-test-subj="bulk-close-alert-on-add-add-exception-checkbox"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index dcc4163fa9bf5..21fc23a1a72fa 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -8,7 +8,7 @@ import type { Exception } from '../objects/exception'; import { RULE_STATUS } from '../screens/create_new_rule'; import { - ADD_EXCEPTIONS_BTN, + ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN, CLOSE_ALERTS_CHECKBOX, CONFIRM_BTN, FIELD_INPUT, @@ -66,9 +66,12 @@ export const addsFieldsToTimeline = (search: string, fields: string[]) => { }; export const openExceptionFlyoutFromRuleSettings = () => { - cy.get(ADD_EXCEPTIONS_BTN).click(); - cy.get(LOADING_SPINNER).should('not.exist'); - cy.get(FIELD_INPUT).should('be.visible'); + cy.root() + .pipe(($el) => { + $el.find(ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN).trigger('click'); + return $el.find(FIELD_INPUT); + }) + .should('be.visible'); }; export const addsExceptionFromRuleSettings = (exception: Exception) => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx index 49af0d2f9a097..69bf7fad77161 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useReducer, useState } from 'react'; +import React, { useCallback, useMemo, useEffect, useReducer, useState } from 'react'; import { EuiPanel, EuiSpacer } from '@elastic/eui'; import type { @@ -57,9 +57,14 @@ const initialState: State = { exceptionToEdit: null, currenFlyout: null, viewerState: 'loading', - exceptionLists: [], }; +export interface GetExceptionItemProps { + pagination?: Partial; + search?: string; + filters?: string; +} + interface ExceptionsViewerProps { rule: Rule | null; listType: ExceptionListTypeEnum; @@ -76,14 +81,19 @@ const ExceptionsViewerComponent = ({ const [{ canUserCRUD, hasIndexWrite }] = useUserData(); const [isReadOnly, setReadOnly] = useState(true); const [lastUpdated, setLastUpdated] = useState(null); + const exceptionListsToQuery = useMemo( + () => + rule != null && rule.exceptions_list != null + ? rule.exceptions_list.filter((list) => list.type === listType) + : [], + [listType, rule] + ); // Reducer state - const [ - { exceptions, pagination, currenFlyout, exceptionToEdit, viewerState, exceptionLists }, - dispatch, - ] = useReducer(allExceptionItemsReducer(), { - ...initialState, - }); + const [{ exceptions, pagination, currenFlyout, exceptionToEdit, viewerState }, dispatch] = + useReducer(allExceptionItemsReducer(), { + ...initialState, + }); // Reducer actions const setExceptions = useCallback( @@ -112,16 +122,6 @@ const ExceptionsViewerComponent = ({ [dispatch] ); - const setExceptionLists = useCallback( - (lists: ListArray): void => { - dispatch({ - type: 'setExceptionLists', - lists, - }); - }, - [dispatch] - ); - const setFlyoutType = useCallback( (flyoutType: ViewerFlyoutName): void => { dispatch({ @@ -132,10 +132,10 @@ const ExceptionsViewerComponent = ({ [dispatch] ); - const [_, allReferences] = useFindExceptionListReferences(exceptionLists); + const [_, allReferences] = useFindExceptionListReferences(exceptionListsToQuery); const handleFetchItems = useCallback( - async (options?: { pagination?: Partial; search?: string; filters?: string }) => { + async (options?: GetExceptionItemProps) => { const abortCtrl = new AbortController(); const newPagination = @@ -148,6 +148,16 @@ const ExceptionsViewerComponent = ({ page: pagination.pageIndex + 1, perPage: pagination.pageSize, }; + + if (exceptionListsToQuery.length === 0) { + return { + data: [], + pageIndex: pagination.pageIndex, + itemsPerPage: pagination.pageSize, + total: 0, + }; + } + const { page: pageIndex, per_page: itemsPerPage, @@ -156,8 +166,8 @@ const ExceptionsViewerComponent = ({ } = await fetchExceptionListsItemsByListIds({ filter: undefined, http: services.http, - listIds: exceptionLists.map((list) => list.list_id), - namespaceTypes: exceptionLists.map((list) => list.namespace_type), + listIds: exceptionListsToQuery.map((list) => list.list_id), + namespaceTypes: exceptionListsToQuery.map((list) => list.namespace_type), search: options?.search, pagination: newPagination, signal: abortCtrl.signal, @@ -174,20 +184,15 @@ const ExceptionsViewerComponent = ({ total, }; }, - [pagination.pageIndex, pagination.pageSize, exceptionLists, services.http] + [pagination.pageIndex, pagination.pageSize, exceptionListsToQuery, services.http] ); const handleGetExceptionListItems = useCallback( - async (options?: { page: number; perPage: number }) => { + async (options?: GetExceptionItemProps) => { try { setViewerState('loading'); - const { pageIndex, itemsPerPage, total, data } = await handleFetchItems({ - pagination: { - page: options?.page ?? pagination.pageIndex, - perPage: options?.perPage ?? pagination.pageSize, - }, - }); + const { pageIndex, itemsPerPage, total, data } = await handleFetchItems(options); setViewerState(total > 0 ? null : 'empty'); @@ -208,22 +213,15 @@ const ExceptionsViewerComponent = ({ }); } }, - [ - handleFetchItems, - setExceptions, - setViewerState, - pagination.pageSize, - pagination.pageIndex, - toasts, - ] + [handleFetchItems, setExceptions, setViewerState, toasts] ); const handleSearch = useCallback( - async (search: string) => { + async (options?: GetExceptionItemProps) => { try { setViewerState('searching'); - const { pageIndex, itemsPerPage, total, data } = await handleFetchItems({ search }); + const { pageIndex, itemsPerPage, total, data } = await handleFetchItems(options); setViewerState(total > 0 ? null : 'empty_search'); @@ -311,26 +309,16 @@ const ExceptionsViewerComponent = ({ }, [setReadOnly, canUserCRUD, hasIndexWrite]); useEffect(() => { - if (rule != null) { - const lists = - rule != null && rule.exceptions_list != null - ? rule.exceptions_list.filter((list) => list.type === listType) - : []; - setExceptionLists(lists); - } else { - setViewerState('loading'); - } - }, [listType, rule, setViewerState, setExceptionLists]); - - useEffect(() => { - if (exceptionLists.length > 0) { + if (exceptionListsToQuery.length > 0) { handleGetExceptionListItems(); + } else { + setViewerState('empty'); } - }, [exceptionLists, handleGetExceptionListItems, setViewerState]); + }, [exceptionListsToQuery.length, handleGetExceptionListItems, setViewerState]); return ( <> - {currenFlyout === 'editException' && exceptionToEdit != null && ( + {currenFlyout === 'editException' && exceptionToEdit != null && rule != null && ( )} - {currenFlyout === 'addException' && ( + {currenFlyout === 'addException' && rule != null && ( void; + onPaginationChange: (arg: GetExceptionItemProps) => void; } const ExceptionsViewerPaginationComponent = ({ @@ -23,8 +24,10 @@ const ExceptionsViewerPaginationComponent = ({ const handleItemsPerPageChange = useCallback( (pageSize: number) => { onPaginationChange({ - page: pagination.pageIndex, - perPage: pageSize, + pagination: { + page: pagination.pageIndex, + perPage: pageSize, + }, }); }, [onPaginationChange, pagination.pageIndex] @@ -33,8 +36,10 @@ const ExceptionsViewerPaginationComponent = ({ const handlePageIndexChange = useCallback( (pageIndex: number) => { onPaginationChange({ - page: pageIndex, - perPage: pagination.pageSize, + pagination: { + page: pageIndex, + perPage: pagination.pageSize, + }, }); }, [onPaginationChange, pagination.pageSize] diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/reducer.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/reducer.ts index a93f473f3c0b5..dbf617e55f66f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/reducer.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/reducer.ts @@ -5,11 +5,7 @@ * 2.0. */ -import type { - ExceptionListItemSchema, - Pagination, - ListArray, -} from '@kbn/securitysolution-io-ts-list-types'; +import type { ExceptionListItemSchema, Pagination } from '@kbn/securitysolution-io-ts-list-types'; import type { ExceptionsPagination } from '../../utils/types'; export type ViewerFlyoutName = 'addException' | 'editException' | null; @@ -31,8 +27,6 @@ export interface State { // Flyout to be opened (edit vs add vs none) currenFlyout: ViewerFlyoutName; viewerState: ViewerState; - // Exception list containers - exceptionLists: ListArray; } export type Action = @@ -41,10 +35,6 @@ export type Action = exceptions: ExceptionListItemSchema[]; pagination: Pagination; } - | { - type: 'setExceptionLists'; - lists: ListArray; - } | { type: 'updateFlyoutOpen'; flyoutType: ViewerFlyoutName } | { type: 'updateExceptionToEdit'; @@ -73,12 +63,6 @@ export const allExceptionItemsReducer = exceptions, }; } - case 'setExceptionLists': { - return { - ...state, - exceptionLists: action.lists, - }; - } case 'updateExceptionToEdit': { const { exception } = action; return { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx index 9c0651c8979c8..20133d54b94d5 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx @@ -10,6 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiSearchBar } from '@elastic/eui import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import * as i18n from '../../utils/translations'; +import type { GetExceptionItemProps } from '.'; const ITEMS_SCHEMA = { strict: true, @@ -47,7 +48,7 @@ interface ExceptionsViewerSearchBarProps { // being created when "onAddExceptionClick" is invoked listType: ExceptionListTypeEnum; isSearching: boolean; - onSearch: (arg: string) => void; + onSearch: (arg: GetExceptionItemProps) => void; onAddExceptionClick: (type: ExceptionListTypeEnum) => void; } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.test.tsx index 4972090b7c049..5219f5d72d847 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.test.tsx @@ -246,6 +246,7 @@ describe('ExceptionItemCard', () => { expect(mockOnDeleteException).toHaveBeenCalledWith({ id: '1', + name: 'some name', namespaceType: 'single', }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index c4585d5e6e9ef..6c5ff0ab2851e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -317,8 +317,10 @@ describe('RuleDetailsPageComponent', () => { ); await waitFor(() => { - expect(wrapper.find('[data-test-subj="exceptionTab"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="endpointExceptionsTab"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="navigation-rule_exceptions"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="navigation-endpoint_exceptions"]').exists() + ).toBeFalsy(); }); }); @@ -352,8 +354,10 @@ describe('RuleDetailsPageComponent', () => { ); await waitFor(() => { - expect(wrapper.find('[data-test-subj="exceptionTab"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="endpointExceptionsTab"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="navigation-rule_exceptions"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="navigation-endpoint_exceptions"]').exists() + ).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.test.ts index de33a55524168..9287cd782518d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.test.ts @@ -49,7 +49,7 @@ describe('findRuleExceptionReferencesRoute', () => { test('returns 200 when adding an exception item and rule_default exception list already exists', async () => { const request = requestMock.create({ method: 'get', - path: `${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, + path: `${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references?exception_list`, query: { ids: `4656dc92-5832-11ea-8e2d-0242ac130003`, list_ids: `my_default_list`, @@ -127,7 +127,7 @@ describe('findRuleExceptionReferencesRoute', () => { expect(response.status).toEqual(400); expect(response.body).toEqual({ message: - '"ids", "list_ids" and "namespace_types" need to have the same comma separated number of values. Expected "ids" length: 1 to equal "namespace_types" length: 2 and "list_ids" legnth: 1.', + '"ids", "list_ids" and "namespace_types" need to have the same comma separated number of values. Expected "ids" length: 1 to equal "namespace_types" length: 2 and "list_ids" length: 1.', status_code: 400, }); }); From 6a5c4b23c56cb886c7057d9c5117008022bb43b3 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 30 Aug 2022 23:25:15 -0700 Subject: [PATCH 25/30] cleanup of code --- .../components/exception_item_card/meta.tsx | 31 +++++--------- .../rule_exceptions/utils/translations.ts | 40 ------------------- .../translations/translations/fr-FR.json | 6 --- .../translations/translations/ja-JP.json | 6 --- .../translations/translations/zh-CN.json | 6 --- 5 files changed, 9 insertions(+), 80 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.tsx index 9bcbb353244e1..517f10d43b506 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.tsx @@ -23,12 +23,11 @@ import styled from 'styled-components'; import * as i18n from './translations'; import { FormattedDate } from '../../../../common/components/formatted_date'; -import { LinkAnchor } from '../../../../common/components/links'; -import { APP_UI_ID, SecurityPageName } from '../../../../../common/constants'; -import { useKibana } from '../../../../common/lib/kibana'; -import { getRuleDetailsUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; -import { useGetSecuritySolutionUrl } from '../../../../common/components/link_to'; +import { SecurityPageName } from '../../../../../common/constants'; import type { RuleReferenceSchema } from '../../../../../common/detection_engine/schemas/response'; +import { SecuritySolutionLinkAnchor } from '../../../../common/components/links'; +import { RuleDetailTabs } from '../../../../detections/pages/detection_engine/rules/details'; +import { getRuleDetailsTabUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; const StyledFlexItem = styled(EuiFlexItem)` border-right: 1px solid #d3dae6; @@ -43,9 +42,6 @@ export interface ExceptionItemCardMetaInfoProps { export const ExceptionItemCardMetaInfo = memo( ({ item, references, dataTestSubj }) => { - const { navigateToApp } = useKibana().services.application; - const getSecuritySolutionUrl = useGetSecuritySolutionUrl(); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); const onAffectedRulesClick = () => setIsPopoverOpen((isOpen) => !isOpen); @@ -61,26 +57,17 @@ export const ExceptionItemCardMetaInfo = memo( key={reference.id} > - void }) => { - ev.preventDefault(); - navigateToApp(APP_UI_ID, { - deepLinkId: SecurityPageName.rules, - path: getRuleDetailsUrl(reference.id), - }); - }} - href={getSecuritySolutionUrl({ - deepLinkId: SecurityPageName.rules, - path: getRuleDetailsUrl(reference.id), - })} + deepLinkId={SecurityPageName.rules} + path={getRuleDetailsTabUrl(reference.id, RuleDetailTabs.alerts)} > {reference.name} - + )); - }, [references, dataTestSubj, getSecuritySolutionUrl, navigateToApp]); + }, [references, dataTestSubj]); return ( - i18n.translate('xpack.securitySolution.exceptions.exceptionItemsPaginationLabel', { - values: { items }, - defaultMessage: 'Rows per page: {items}', - }); - -export const NUMBER_OF_ITEMS = (items: number) => - i18n.translate('xpack.securitySolution.exceptions.paginationNumberOfItemsLabel', { - values: { items }, - defaultMessage: '{items} items', - }); - export const ADD_COMMENT_PLACEHOLDER = i18n.translate( 'xpack.securitySolution.exceptions.viewer.addCommentPlaceholder', { @@ -91,13 +58,6 @@ export const ADD_TO_CLIPBOARD = i18n.translate( } ); -export const TOTAL_ITEMS_FETCH_ERROR = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.fetchTotalsError', - { - defaultMessage: 'Error getting exception item totals', - } -); - export const CLEAR_EXCEPTIONS_LABEL = i18n.translate( 'xpack.securitySolution.exceptions.clearExceptionsLabel', { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index ff888d45d7eea..63411a4e38b00 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -25463,7 +25463,6 @@ "xpack.securitySolution.exceptions.failedLoadPolicies": "Une erreur s'est produite lors du chargement des politiques : \"{error}\"", "xpack.securitySolution.exceptions.fetch404Error": "La liste d'exceptions associée ({listId}) n'existe plus. Veuillez retirer la liste d'exceptions manquante pour ajouter des exceptions supplémentaires à la règle de détection.", "xpack.securitySolution.exceptions.hideCommentsLabel": "Masquer ({comments}) {comments, plural, =1 {commentaire} other {commentaires}}", - "xpack.securitySolution.exceptions.paginationNumberOfItemsLabel": "{items} éléments", "xpack.securitySolution.exceptions.referenceModalDescription": "Cette liste d'exceptions est associée à ({referenceCount}) {referenceCount, plural, =1 {règle} other {règles}}. Le retrait de cette liste d'exceptions supprimera également sa référence des règles associées.", "xpack.securitySolution.exceptions.referenceModalSuccessDescription": "Liste d'exceptions - {listId} - supprimée avec succès.", "xpack.securitySolution.exceptions.showCommentsLabel": "Afficher ({comments}) {comments, plural, =1 {commentaire} other {commentaires}}", @@ -28086,7 +28085,6 @@ "xpack.securitySolution.exceptions.operatingSystemMac": "macOS", "xpack.securitySolution.exceptions.operatingSystemWindows": "Windows", "xpack.securitySolution.exceptions.operatingSystemWindowsAndMac": "Windows et macOS", - "xpack.securitySolution.exceptions.paginationNumberOfItemsLabel": "{items} éléments", "xpack.securitySolution.exceptions.referenceModalCancelButton": "Annuler", "xpack.securitySolution.exceptions.referenceModalDeleteButton": "Retirer la liste d'exceptions", "xpack.securitySolution.exceptions.referenceModalTitle": "Retirer la liste d'exceptions", @@ -28096,10 +28094,6 @@ "xpack.securitySolution.exceptions.viewer.addToClipboard": "Commentaire", "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "Ajouter une exception à une règle", "xpack.securitySolution.exceptions.viewer.addToEndpointListLabel": "Ajouter une exception de point de terminaison", - "xpack.securitySolution.exceptions.viewer.deleteExceptionError": "Erreur lors de la suppression de l'exception", - "xpack.securitySolution.exceptions.viewer.fetchingListError": "Erreur lors de la récupération des exceptions", - "xpack.securitySolution.exceptions.viewer.fetchTotalsError": "Erreur lors de l'obtention des totaux d'éléments de l'exception", - "xpack.securitySolution.exceptions.viewer.searchDefaultPlaceholder": "Champ de recherche (ex : host.name)", "xpack.securitySolution.exitFullScreenButton": "Quitter le plein écran", "xpack.securitySolution.expandedValue.hideTopValues.HideTopValues": "Masquer les valeurs les plus élevées", "xpack.securitySolution.expandedValue.links.expandIpDetails": "Développer les détails d'IP", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 12f1a970c1ad7..56248e760a156 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25440,7 +25440,6 @@ "xpack.securitySolution.exceptions.failedLoadPolicies": "ポリシーの読み込みエラーが発生しました:\"{error}\"", "xpack.securitySolution.exceptions.fetch404Error": "関連付けられた例外リスト({listId})は存在しません。その他の例外を検出ルールに追加するには、見つからない例外リストを削除してください。", "xpack.securitySolution.exceptions.hideCommentsLabel": "({comments}){comments, plural, other {件のコメント}}を非表示", - "xpack.securitySolution.exceptions.paginationNumberOfItemsLabel": "{items}個の項目", "xpack.securitySolution.exceptions.referenceModalDescription": "この例外リストは、({referenceCount}) {referenceCount, plural, other {個のルール}}に関連付けられています。この例外リストを削除すると、関連付けられたルールからの参照も削除されます。", "xpack.securitySolution.exceptions.referenceModalSuccessDescription": "例外リスト{listId}が正常に削除されました。", "xpack.securitySolution.exceptions.showCommentsLabel": "({comments}){comments, plural, other {件のコメント}}を表示", @@ -28062,7 +28061,6 @@ "xpack.securitySolution.exceptions.operatingSystemMac": "macOS", "xpack.securitySolution.exceptions.operatingSystemWindows": "Windows", "xpack.securitySolution.exceptions.operatingSystemWindowsAndMac": "WindowsおよびmacOS", - "xpack.securitySolution.exceptions.paginationNumberOfItemsLabel": "{items}個の項目", "xpack.securitySolution.exceptions.referenceModalCancelButton": "キャンセル", "xpack.securitySolution.exceptions.referenceModalDeleteButton": "例外リストを削除", "xpack.securitySolution.exceptions.referenceModalTitle": "例外リストを削除", @@ -28072,10 +28070,6 @@ "xpack.securitySolution.exceptions.viewer.addToClipboard": "コメント", "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "ルール例外の追加", "xpack.securitySolution.exceptions.viewer.addToEndpointListLabel": "エンドポイント例外の追加", - "xpack.securitySolution.exceptions.viewer.deleteExceptionError": "例外の削除エラー", - "xpack.securitySolution.exceptions.viewer.fetchingListError": "例外の取得エラー", - "xpack.securitySolution.exceptions.viewer.fetchTotalsError": "例外項目合計数の取得エラー", - "xpack.securitySolution.exceptions.viewer.searchDefaultPlaceholder": "検索フィールド(例:host.name)", "xpack.securitySolution.exitFullScreenButton": "全画面を終了", "xpack.securitySolution.expandedValue.hideTopValues.HideTopValues": "上位の値を非表示", "xpack.securitySolution.expandedValue.links.expandIpDetails": "IP詳細を展開", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f14a06225737c..a61dda14fa42d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25471,7 +25471,6 @@ "xpack.securitySolution.exceptions.failedLoadPolicies": "加载策略时出错:“{error}”", "xpack.securitySolution.exceptions.fetch404Error": "关联的例外列表 ({listId}) 已不存在。请移除缺少的例外列表,以将其他例外添加到检测规则。", "xpack.securitySolution.exceptions.hideCommentsLabel": "隐藏 ({comments}) 个{comments, plural, other {注释}}", - "xpack.securitySolution.exceptions.paginationNumberOfItemsLabel": "{items} 项", "xpack.securitySolution.exceptions.referenceModalDescription": "此例外列表与 ({referenceCount}) 个{referenceCount, plural, other {规则}}关联。移除此例外列表还将会删除其对关联规则的引用。", "xpack.securitySolution.exceptions.referenceModalSuccessDescription": "例外列表 - {listId} - 已成功删除。", "xpack.securitySolution.exceptions.showCommentsLabel": "显示 ({comments} 个) {comments, plural, other {注释}}", @@ -28093,7 +28092,6 @@ "xpack.securitySolution.exceptions.operatingSystemMac": "macOS", "xpack.securitySolution.exceptions.operatingSystemWindows": "Windows", "xpack.securitySolution.exceptions.operatingSystemWindowsAndMac": "Windows 和 macOS", - "xpack.securitySolution.exceptions.paginationNumberOfItemsLabel": "{items} 项", "xpack.securitySolution.exceptions.referenceModalCancelButton": "取消", "xpack.securitySolution.exceptions.referenceModalDeleteButton": "移除例外列表", "xpack.securitySolution.exceptions.referenceModalTitle": "移除例外列表", @@ -28103,10 +28101,6 @@ "xpack.securitySolution.exceptions.viewer.addToClipboard": "注释", "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "添加规则例外", "xpack.securitySolution.exceptions.viewer.addToEndpointListLabel": "添加终端例外", - "xpack.securitySolution.exceptions.viewer.deleteExceptionError": "删除例外时出错", - "xpack.securitySolution.exceptions.viewer.fetchingListError": "提取例外时出错", - "xpack.securitySolution.exceptions.viewer.fetchTotalsError": "获取例外项总数时出错", - "xpack.securitySolution.exceptions.viewer.searchDefaultPlaceholder": "搜索字段(例如:host.name)", "xpack.securitySolution.exitFullScreenButton": "退出全屏", "xpack.securitySolution.expandedValue.hideTopValues.HideTopValues": "隐藏排名最前值", "xpack.securitySolution.expandedValue.links.expandIpDetails": "展开 IP 详情", From 0124115dcb7667952a022776bf08cfae3660b217 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 31 Aug 2022 11:05:57 -0700 Subject: [PATCH 26/30] remove unused logger --- x-pack/plugins/security_solution/server/routes/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 4ec57e8f5db8d..f8773d7e3b2ef 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -131,7 +131,7 @@ export const initRoutes = ( patchTimelinesRoute(router, config, security); importRulesRoute(router, config, ml); exportRulesRoute(router, config, logger); - findRuleExceptionReferencesRoute(router, logger); + findRuleExceptionReferencesRoute(router); importTimelinesRoute(router, config, security); exportTimelinesRoute(router, config, security); From 01cea4fff1cc892493b2b28e8cf66c411923b89d Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 1 Sep 2022 18:06:25 -0700 Subject: [PATCH 27/30] adds cypress tests and updates their folder structure to make a bit more sense with the direction were moving in with updated UX --- .../__tests__/enumerate_patterns.test.js | 10 - .../add_edit_data_view_exception.spec.ts | 159 ------------- .../exceptions/add_edit_exception.spec.ts | 157 ------------- .../alerts_table_flow/add_exception.spec.ts | 102 ++++++++ .../exceptions/exceptions_flyout.spec.ts | 4 +- .../all_exception_lists_read_only.spec.ts | 20 +- .../rule_details_flow/add_exception.spec.ts | 221 ++++++++++++++++++ .../add_exception_data_view.spect.ts | 114 +++++++++ .../rule_details_flow/edit_exception.spec.ts | 154 ++++++++++++ .../edit_exception_data_view.spec.ts | 155 ++++++++++++ .../rule_details_flow/read_only_view.spect.ts | 108 +++++++++ .../cypress/screens/exceptions.ts | 20 +- .../cypress/screens/rule_details.ts | 2 +- .../cypress/tasks/api_calls/rules.ts | 2 + .../cypress/tasks/rule_details.ts | 39 +++- .../all_exception_items_table/all_items.tsx | 3 + .../empty_viewer_state.test.tsx | 4 + .../empty_viewer_state.tsx | 5 +- .../all_exception_items_table/index.tsx | 1 + .../pagination.test.tsx | 6 +- .../all_exception_items_table/search_bar.tsx | 3 +- .../es_archives/exceptions_2/data.json | 31 ++- 22 files changed, 968 insertions(+), 352 deletions(-) delete mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/add_edit_data_view_exception.spec.ts delete mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/add_edit_exception.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_table_flow/add_exception.spec.ts rename x-pack/plugins/security_solution/cypress/integration/exceptions/{ => exceptions_management_flow}/all_exception_lists_read_only.spec.ts (70%) create mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception_data_view.spect.ts create mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception_data_view.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/read_only_view.spect.ts diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js index 407fc32e5a8f9..06428c8935282 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js @@ -37,14 +37,4 @@ describe(`enumeratePatterns`, () => { 'src/plugins/charts/common/static/color_maps/color_maps.ts kibana-app' ); }); - it(`should resolve x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/translations.ts to kibana-security`, () => { - const short = 'x-pack/plugins/security_solution'; - const actual = enumeratePatterns(REPO_ROOT)(log)(new Map([[short, ['kibana-security']]])); - - expect( - actual[0].includes( - `${short}/public/detection_engine/rule_exceptions/components/edit_exception_flyout/translations.ts` - ) - ).toBe(true); - }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/add_edit_data_view_exception.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/add_edit_data_view_exception.spec.ts deleted file mode 100644 index c818f4e51060f..0000000000000 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/add_edit_data_view_exception.spec.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* - * 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 { getException } from '../../objects/exception'; -import { getNewRule } from '../../objects/rule'; - -import { ALERTS_COUNT, EMPTY_ALERT_TABLE, NUMBER_OF_ALERTS } from '../../screens/alerts'; - -import { addExceptionFromFirstAlert, goToClosedAlerts, goToOpenedAlerts } from '../../tasks/alerts'; -import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; -import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; -import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; -import { esArchiverLoad, esArchiverUnload, esArchiverResetKibana } from '../../tasks/es_archiver'; -import { login, visitWithoutDateRange } from '../../tasks/login'; -import { - addsException, - addsExceptionFromRuleSettings, - editException, - goToAlertsTab, - goToExceptionsTab, - removeException, - waitForTheRuleToBeExecuted, -} from '../../tasks/rule_details'; - -import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; -import { deleteAlertsAndRules, postDataView } from '../../tasks/common'; -import { - EXCEPTION_EDIT_FLYOUT_SAVE_BTN, - EXCEPTION_ITEM_CONTAINER, - FIELD_INPUT, -} from '../../screens/exceptions'; -import { - addExceptionEntryFieldValueOfItemX, - addExceptionEntryFieldValueValue, -} from '../../tasks/exceptions'; - -describe('Adds rule exception using data views', () => { - const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; - - before(() => { - esArchiverResetKibana(); - esArchiverLoad('exceptions'); - login(); - - postDataView('exceptions-*'); - }); - - beforeEach(() => { - deleteAlertsAndRules(); - createCustomRuleEnabled( - { - ...getNewRule(), - customQuery: 'agent.name:*', - dataSource: { dataView: 'exceptions-*', type: 'dataView' }, - }, - 'rule_testing', - '1s' - ); - visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - goToRuleDetails(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - }); - - afterEach(() => { - esArchiverUnload('exceptions_2'); - }); - - after(() => { - esArchiverUnload('exceptions'); - }); - - it('Creates an exception from an alert and deletes it', () => { - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS); - // Create an exception from the alerts actions menu that matches - // the existing alert - addExceptionFromFirstAlert(); - addsException(getException()); - - // Alerts table should now be empty from having added exception and closed - // matching alert - cy.get(EMPTY_ALERT_TABLE).should('exist'); - - // Closed alert should appear in table - goToClosedAlerts(); - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - - // Remove the exception and load an event that would have matched that exception - // to show that said exception now starts to show up again - goToExceptionsTab(); - removeException(); - esArchiverLoad('exceptions_2'); - goToAlertsTab(); - goToOpenedAlerts(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - }); - - it('Creates an exception from a rule and deletes it', () => { - // Create an exception from the exception tab that matches - // the existing alert - goToExceptionsTab(); - addsExceptionFromRuleSettings(getException()); - - // Alerts table should now be empty from having added exception and closed - // matching alert - goToAlertsTab(); - cy.get(EMPTY_ALERT_TABLE).should('exist'); - - // Closed alert should appear in table - goToClosedAlerts(); - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - - // Remove the exception and load an event that would have matched that exception - // to show that said exception now starts to show up again - goToExceptionsTab(); - removeException(); - esArchiverLoad('exceptions_2'); - goToAlertsTab(); - goToOpenedAlerts(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - }); - - it('Edits an exception', () => { - goToExceptionsTab(); - addsExceptionFromRuleSettings(getException()); - - editException(); - - // check that the existing item's field is being populated - cy.get(EXCEPTION_ITEM_CONTAINER) - .eq(0) - .find(FIELD_INPUT) - .eq(0) - .should('have.text', 'agent.name'); - - // check that you can select a different field - addExceptionEntryFieldValueOfItemX('user.name{downarrow}{enter}', 0, 0); - addExceptionEntryFieldValueValue('test', 0); - - cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).click(); - cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('have.attr', 'disabled'); - cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('not.exist'); - }); -}); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/add_edit_exception.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/add_edit_exception.spec.ts deleted file mode 100644 index 886f772bc25e5..0000000000000 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/add_edit_exception.spec.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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 { getException } from '../../objects/exception'; -import { getNewRule } from '../../objects/rule'; - -import { ALERTS_COUNT, EMPTY_ALERT_TABLE, NUMBER_OF_ALERTS } from '../../screens/alerts'; - -import { addExceptionFromFirstAlert, goToClosedAlerts, goToOpenedAlerts } from '../../tasks/alerts'; -import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; -import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; -import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; -import { esArchiverLoad, esArchiverUnload, esArchiverResetKibana } from '../../tasks/es_archiver'; -import { login, visitWithoutDateRange } from '../../tasks/login'; -import { - addsException, - addsExceptionFromRuleSettings, - editException, - goToAlertsTab, - goToExceptionsTab, - removeException, - waitForTheRuleToBeExecuted, -} from '../../tasks/rule_details'; - -import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; -import { deleteAlertsAndRules } from '../../tasks/common'; -import { - EXCEPTION_EDIT_FLYOUT_SAVE_BTN, - EXCEPTION_ITEM_CONTAINER, - FIELD_INPUT, -} from '../../screens/exceptions'; -import { - addExceptionEntryFieldValueOfItemX, - addExceptionEntryFieldValueValue, -} from '../../tasks/exceptions'; - -describe('Adds rule exception', () => { - const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; - - before(() => { - esArchiverResetKibana(); - esArchiverLoad('exceptions'); - login(); - }); - - beforeEach(() => { - deleteAlertsAndRules(); - createCustomRuleEnabled( - { - ...getNewRule(), - customQuery: 'agent.name:*', - dataSource: { index: ['exceptions*'], type: 'indexPatterns' }, - }, - 'rule_testing', - '1s' - ); - visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - goToRuleDetails(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - }); - - afterEach(() => { - esArchiverUnload('exceptions_2'); - }); - - after(() => { - esArchiverUnload('exceptions'); - }); - - it('Creates an exception from an alert and deletes it', () => { - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS); - // Create an exception from the alerts actions menu that matches - // the existing alert - addExceptionFromFirstAlert(); - addsException(getException()); - - // Alerts table should now be empty from having added exception and closed - // matching alert - cy.get(EMPTY_ALERT_TABLE).should('exist'); - - // Closed alert should appear in table - goToClosedAlerts(); - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - - // Remove the exception and load an event that would have matched that exception - // to show that said exception now starts to show up again - goToExceptionsTab(); - removeException(); - esArchiverLoad('exceptions_2'); - goToAlertsTab(); - goToOpenedAlerts(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - }); - - it('Creates an exception from a rule and deletes it', () => { - // Create an exception from the exception tab that matches - // the existing alert - goToExceptionsTab(); - addsExceptionFromRuleSettings(getException()); - - // Alerts table should now be empty from having added exception and closed - // matching alert - goToAlertsTab(); - cy.get(EMPTY_ALERT_TABLE).should('exist'); - - // Closed alert should appear in table - goToClosedAlerts(); - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - - // Remove the exception and load an event that would have matched that exception - // to show that said exception now starts to show up again - goToExceptionsTab(); - removeException(); - esArchiverLoad('exceptions_2'); - goToAlertsTab(); - goToOpenedAlerts(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - }); - - it('Edits an exception', () => { - goToExceptionsTab(); - addsExceptionFromRuleSettings(getException()); - - editException(); - - // check that the existing item's field is being populated - cy.get(EXCEPTION_ITEM_CONTAINER) - .eq(0) - .find(FIELD_INPUT) - .eq(0) - .should('have.text', 'agent.name'); - - // check that you can select a different field - addExceptionEntryFieldValueOfItemX('user.name{downarrow}{enter}', 0, 0); - addExceptionEntryFieldValueValue('test', 0); - - cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).click(); - cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('have.attr', 'disabled'); - cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('not.exist'); - }); -}); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_table_flow/add_exception.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_table_flow/add_exception.spec.ts new file mode 100644 index 0000000000000..58e58b3ba2f83 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_table_flow/add_exception.spec.ts @@ -0,0 +1,102 @@ +/* + * 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 { getException } from '../../../objects/exception'; +import { getNewRule } from '../../../objects/rule'; + +import { ALERTS_COUNT, EMPTY_ALERT_TABLE, NUMBER_OF_ALERTS } from '../../../screens/alerts'; + +import { + addExceptionFromFirstAlert, + goToClosedAlerts, + goToOpenedAlerts, +} from '../../../tasks/alerts'; +import { createCustomRuleEnabled } from '../../../tasks/api_calls/rules'; +import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; +import { + esArchiverLoad, + esArchiverUnload, + esArchiverResetKibana, +} from '../../../tasks/es_archiver'; +import { login, visitWithoutDateRange } from '../../../tasks/login'; +import { + addsException, + goToAlertsTab, + goToExceptionsTab, + removeException, + waitForTheRuleToBeExecuted, +} from '../../../tasks/rule_details'; + +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; +import { deleteAlertsAndRules } from '../../../tasks/common'; + +describe('Adds rule exception from alerts flow', () => { + const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; + + before(() => { + esArchiverResetKibana(); + esArchiverLoad('exceptions'); + login(); + }); + + beforeEach(() => { + deleteAlertsAndRules(); + createCustomRuleEnabled( + { + ...getNewRule(), + customQuery: 'agent.name:*', + dataSource: { index: ['exceptions*'], type: 'indexPatterns' }, + }, + 'rule_testing', + '1s' + ); + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + }); + + afterEach(() => { + esArchiverUnload('exceptions_2'); + }); + + after(() => { + esArchiverUnload('exceptions'); + }); + + it('Creates an exception from an alert and deletes it', () => { + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS); + // Create an exception from the alerts actions menu that matches + // the existing alert + addExceptionFromFirstAlert(); + addsException(getException()); + + // Alerts table should now be empty from having added exception and closed + // matching alert + cy.get(EMPTY_ALERT_TABLE).should('exist'); + + // Closed alert should appear in table + goToClosedAlerts(); + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + + // Remove the exception and load an event that would have matched that exception + // to show that said exception now starts to show up again + goToExceptionsTab(); + removeException(); + esArchiverLoad('exceptions_2'); + goToAlertsTab(); + goToOpenedAlerts(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts index 2b3f39150dd76..dfb018b4bfb5a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts @@ -14,7 +14,7 @@ import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; import { esArchiverLoad, esArchiverResetKibana, esArchiverUnload } from '../../tasks/es_archiver'; import { login, visitWithoutDateRange } from '../../tasks/login'; import { - openExceptionFlyoutFromRuleSettings, + openExceptionFlyoutFromEmptyViewerPrompt, goToExceptionsTab, editException, } from '../../tasks/rule_details'; @@ -201,7 +201,7 @@ describe('Exceptions flyout', () => { }); it('Does not overwrite values of nested entry items', () => { - openExceptionFlyoutFromRuleSettings(); + openExceptionFlyoutFromEmptyViewerPrompt(); cy.get(LOADING_SPINNER).should('not.exist'); // exception item 1 diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/all_exception_lists_read_only.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_management_flow/all_exception_lists_read_only.spec.ts similarity index 70% rename from x-pack/plugins/security_solution/cypress/integration/exceptions/all_exception_lists_read_only.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_management_flow/all_exception_lists_read_only.spec.ts index e17bc694ab48a..c1c4fc18960e6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/all_exception_lists_read_only.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_management_flow/all_exception_lists_read_only.spec.ts @@ -5,14 +5,18 @@ * 2.0. */ -import { ROLES } from '../../../common/test'; -import { getExceptionList } from '../../objects/exception'; -import { EXCEPTIONS_TABLE_SHOWING_LISTS } from '../../screens/exceptions'; -import { createExceptionList } from '../../tasks/api_calls/exceptions'; -import { dismissCallOut, getCallOut, waitForCallOutToBeShown } from '../../tasks/common/callouts'; -import { esArchiverResetKibana } from '../../tasks/es_archiver'; -import { login, visitWithoutDateRange } from '../../tasks/login'; -import { EXCEPTIONS_URL } from '../../urls/navigation'; +import { ROLES } from '../../../../common/test'; +import { getExceptionList } from '../../../objects/exception'; +import { EXCEPTIONS_TABLE_SHOWING_LISTS } from '../../../screens/exceptions'; +import { createExceptionList } from '../../../tasks/api_calls/exceptions'; +import { + dismissCallOut, + getCallOut, + waitForCallOutToBeShown, +} from '../../../tasks/common/callouts'; +import { esArchiverResetKibana } from '../../../tasks/es_archiver'; +import { login, visitWithoutDateRange } from '../../../tasks/login'; +import { EXCEPTIONS_URL } from '../../../urls/navigation'; const MISSING_PRIVILEGES_CALLOUT = 'missing-user-privileges'; diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception.spec.ts new file mode 100644 index 0000000000000..add3f01798129 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception.spec.ts @@ -0,0 +1,221 @@ +/* + * 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 { getException, getExceptionList } from '../../../objects/exception'; +import { getNewRule } from '../../../objects/rule'; + +import { ALERTS_COUNT, EMPTY_ALERT_TABLE, NUMBER_OF_ALERTS } from '../../../screens/alerts'; +import { createCustomRule, createCustomRuleEnabled } from '../../../tasks/api_calls/rules'; +import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; +import { goToClosedAlerts, goToOpenedAlerts } from '../../../tasks/alerts'; +import { + esArchiverLoad, + esArchiverUnload, + esArchiverResetKibana, +} from '../../../tasks/es_archiver'; +import { login, visitWithoutDateRange } from '../../../tasks/login'; +import { + addExceptionFromRuleDetails, + addFirstExceptionFromRuleDetails, + goToAlertsTab, + goToExceptionsTab, + removeException, + searchForExceptionItem, + waitForTheRuleToBeExecuted, +} from '../../../tasks/rule_details'; + +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; +import { deleteAlertsAndRules } from '../../../tasks/common'; +import { + NO_EXCEPTIONS_EXIST_PROMPT, + EXCEPTION_ITEM_VIEWER_CONTAINER, + NO_EXCEPTIONS_SEARCH_RESULTS_PROMPT, +} from '../../../screens/exceptions'; +import { + createExceptionList, + createExceptionListItem, + deleteExceptionList, +} from '../../../tasks/api_calls/exceptions'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; + +describe('Add exception from rule details', () => { + const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; + + before(() => { + esArchiverResetKibana(); + esArchiverLoad('exceptions'); + login(); + }); + + after(() => { + esArchiverUnload('exceptions'); + }); + + describe('rule with existing exceptions', () => { + const exceptionList = getExceptionList(); + beforeEach(() => { + deleteAlertsAndRules(); + deleteExceptionList(exceptionList.list_id, exceptionList.namespace_type); + // create rule with exceptions + createExceptionList(exceptionList, exceptionList.list_id).then((response) => { + createCustomRule( + { + ...getNewRule(), + customQuery: 'agent.name:*', + dataSource: { index: ['exceptions*'], type: 'indexPatterns' }, + exceptionLists: [ + { + id: response.body.id, + list_id: exceptionList.list_id, + type: exceptionList.type, + namespace_type: exceptionList.namespace_type, + }, + ], + }, + '2' + ); + createExceptionListItem(exceptionList.list_id, { + list_id: exceptionList.list_id, + item_id: 'simple_list_item', + tags: [], + type: 'simple', + description: 'Test exception item', + name: 'Sample Exception List Item', + namespace_type: 'single', + entries: [ + { + field: 'user.name', + operator: 'included', + type: 'match_any', + value: ['bar'], + }, + ], + }); + createExceptionListItem(exceptionList.list_id, { + list_id: exceptionList.list_id, + item_id: 'simple_list_item_2', + tags: [], + type: 'simple', + description: 'Test exception item 2', + name: 'Sample Exception List Item 2', + namespace_type: 'single', + entries: [ + { + field: 'unique_value.test', + operator: 'included', + type: 'match_any', + value: ['foo'], + }, + ], + }); + }); + + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + goToExceptionsTab(); + }); + + it('Creates an exception item', () => { + // displays existing exception items + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 2); + + // clicks prompt button to add a new exception item + addExceptionFromRuleDetails(getException()); + + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 3); + }); + + // Trying to figure out with EUI why the search won't trigger + it.skip('Can search for items', () => { + // displays existing exception items + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 2); + + // can search for an exception value + searchForExceptionItem('foo'); + + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + + // displays empty search result view if no matches found + searchForExceptionItem('abc'); + + // new exception item displays + cy.get(NO_EXCEPTIONS_SEARCH_RESULTS_PROMPT).should('exist'); + }); + }); + + describe('rule without existing exceptions', () => { + beforeEach(() => { + deleteAlertsAndRules(); + createCustomRuleEnabled( + { + ...getNewRule(), + customQuery: 'agent.name:*', + dataSource: { index: ['exceptions*'], type: 'indexPatterns' }, + }, + 'rule_testing', + '1s' + ); + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + goToExceptionsTab(); + }); + + afterEach(() => { + esArchiverUnload('exceptions_2'); + }); + + it('Creates an exception item when none exist', () => { + // when no exceptions exist, empty component shows with action to add exception + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); + + // clicks prompt button to add first exception that will also select to close + // all matching alerts + addFirstExceptionFromRuleDetails({ + field: 'agent.name', + operator: 'is', + values: ['foo'], + }); + + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + + // Alerts table should now be empty from having added exception and closed + // matching alert + goToAlertsTab(); + cy.get(EMPTY_ALERT_TABLE).should('exist'); + + // Closed alert should appear in table + goToClosedAlerts(); + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + + // Remove the exception and load an event that would have matched that exception + // to show that said exception now starts to show up again + goToExceptionsTab(); + + // when removing exception and again, no more exist, empty screen shows again + removeException(); + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); + + // load more docs + esArchiverLoad('exceptions_2'); + + // now that there are no more exceptions, the docs should match and populate alerts + goToAlertsTab(); + goToOpenedAlerts(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', '2 alerts'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception_data_view.spect.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception_data_view.spect.ts new file mode 100644 index 0000000000000..05b21abe52565 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception_data_view.spect.ts @@ -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 { getNewRule } from '../../../objects/rule'; +import { ALERTS_COUNT, EMPTY_ALERT_TABLE, NUMBER_OF_ALERTS } from '../../../screens/alerts'; +import { createCustomRuleEnabled } from '../../../tasks/api_calls/rules'; +import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; +import { goToClosedAlerts, goToOpenedAlerts } from '../../../tasks/alerts'; +import { + esArchiverLoad, + esArchiverUnload, + esArchiverResetKibana, +} from '../../../tasks/es_archiver'; +import { login, visitWithoutDateRange } from '../../../tasks/login'; +import { + addFirstExceptionFromRuleDetails, + goToAlertsTab, + goToExceptionsTab, + removeException, + waitForTheRuleToBeExecuted, +} from '../../../tasks/rule_details'; + +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; +import { postDataView, deleteAlertsAndRules } from '../../../tasks/common'; +import { + NO_EXCEPTIONS_EXIST_PROMPT, + EXCEPTION_ITEM_VIEWER_CONTAINER, +} from '../../../screens/exceptions'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; + +describe('Add exception using data views from rule details', () => { + const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; + + before(() => { + esArchiverResetKibana(); + esArchiverLoad('exceptions'); + login(); + postDataView('exceptions-*'); + }); + + after(() => { + esArchiverUnload('exceptions'); + }); + + beforeEach(() => { + deleteAlertsAndRules(); + createCustomRuleEnabled( + { + ...getNewRule(), + customQuery: 'agent.name:*', + dataSource: { dataView: 'exceptions-*', type: 'dataView' }, + }, + 'rule_testing', + '1s' + ); + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + goToExceptionsTab(); + }); + + afterEach(() => { + esArchiverUnload('exceptions_2'); + }); + + it('Creates an exception item when none exist', () => { + // when no exceptions exist, empty component shows with action to add exception + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); + + // clicks prompt button to add first exception that will also select to close + // all matching alerts + addFirstExceptionFromRuleDetails({ + field: 'agent.name', + operator: 'is', + values: ['foo'], + }); + + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + + // Alerts table should now be empty from having added exception and closed + // matching alert + goToAlertsTab(); + cy.get(EMPTY_ALERT_TABLE).should('exist'); + + // Closed alert should appear in table + goToClosedAlerts(); + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + + // Remove the exception and load an event that would have matched that exception + // to show that said exception now starts to show up again + goToExceptionsTab(); + + // when removing exception and again, no more exist, empty screen shows again + removeException(); + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); + + // load more docs + esArchiverLoad('exceptions_2'); + + // now that there are no more exceptions, the docs should match and populate alerts + goToAlertsTab(); + goToOpenedAlerts(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', '2 alerts'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception.spec.ts new file mode 100644 index 0000000000000..26763c9efeebc --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception.spec.ts @@ -0,0 +1,154 @@ +/* + * 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 { getExceptionList } from '../../../objects/exception'; +import { getNewRule } from '../../../objects/rule'; + +import { createCustomRuleEnabled } from '../../../tasks/api_calls/rules'; +import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; +import { goToOpenedAlerts } from '../../../tasks/alerts'; +import { + esArchiverLoad, + esArchiverUnload, + esArchiverResetKibana, +} from '../../../tasks/es_archiver'; +import { login, visitWithoutDateRange } from '../../../tasks/login'; +import { + goToExceptionsTab, + waitForTheRuleToBeExecuted, + editException, + goToAlertsTab, +} from '../../../tasks/rule_details'; + +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; +import { deleteAlertsAndRules } from '../../../tasks/common'; +import { + EXCEPTION_EDIT_FLYOUT_SAVE_BTN, + EXCEPTION_ITEM_VIEWER_CONTAINER, + EXCEPTION_ITEM_CONTAINER, + FIELD_INPUT, +} from '../../../screens/exceptions'; +import { + createExceptionList, + createExceptionListItem, + deleteExceptionList, +} from '../../../tasks/api_calls/exceptions'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; +import { + addExceptionEntryFieldValueOfItemX, + addExceptionEntryFieldValueValue, +} from '../../../tasks/exceptions'; +import { ALERTS_COUNT, NUMBER_OF_ALERTS } from '../../../screens/alerts'; + +describe('Edit exception from rule details', () => { + const exceptionList = getExceptionList(); + const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; + + before(() => { + esArchiverResetKibana(); + esArchiverLoad('exceptions'); + login(); + }); + + after(() => { + esArchiverUnload('exceptions'); + }); + + beforeEach(() => { + deleteAlertsAndRules(); + deleteExceptionList(exceptionList.list_id, exceptionList.namespace_type); + // create rule with exceptions + createExceptionList(exceptionList, exceptionList.list_id).then((response) => { + createCustomRuleEnabled( + { + ...getNewRule(), + customQuery: 'agent.name:*', + dataSource: { index: ['exceptions*'], type: 'indexPatterns' }, + exceptionLists: [ + { + id: response.body.id, + list_id: exceptionList.list_id, + type: exceptionList.type, + namespace_type: exceptionList.namespace_type, + }, + ], + }, + '2', + '2s' + ); + createExceptionListItem(exceptionList.list_id, { + list_id: exceptionList.list_id, + item_id: 'simple_list_item', + tags: [], + type: 'simple', + description: 'Test exception item', + name: 'Sample Exception List Item', + namespace_type: 'single', + entries: [ + { + field: 'unique_value.test', + operator: 'included', + type: 'match_any', + value: ['bar'], + }, + ], + }); + }); + + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + goToExceptionsTab(); + }); + + afterEach(() => { + esArchiverUnload('exceptions_2'); + }); + + it('Edits an exception item', () => { + // displays existing exception item + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + + editException(); + + // check that the existing item's field is being populated + cy.get(EXCEPTION_ITEM_CONTAINER) + .eq(0) + .find(FIELD_INPUT) + .eq(0) + .should('have.text', 'unique_value.test'); + + // check that you can select a different field + addExceptionEntryFieldValueOfItemX('agent.name{downarrow}{enter}', 0, 0); + addExceptionEntryFieldValueValue('foo', 0); + + cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).click(); + cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('have.attr', 'disabled'); + cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('not.exist'); + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + + // Alerts table should still show single alert + goToAlertsTab(); + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + + // load more docs + esArchiverLoad('exceptions_2'); + + // now that 2 more docs have been added, one should match the edited exception + goToAlertsTab(); + goToOpenedAlerts(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(2); + + // there should be 2 alerts, one is the original alert and the second is for the newly + // matching doc + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', '2 alerts'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception_data_view.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception_data_view.spec.ts new file mode 100644 index 0000000000000..b5178615aa581 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception_data_view.spec.ts @@ -0,0 +1,155 @@ +/* + * 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 { getExceptionList } from '../../../objects/exception'; +import { getNewRule } from '../../../objects/rule'; + +import { createCustomRuleEnabled } from '../../../tasks/api_calls/rules'; +import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; +import { goToOpenedAlerts } from '../../../tasks/alerts'; +import { + esArchiverLoad, + esArchiverUnload, + esArchiverResetKibana, +} from '../../../tasks/es_archiver'; +import { login, visitWithoutDateRange } from '../../../tasks/login'; +import { + goToExceptionsTab, + waitForTheRuleToBeExecuted, + editException, + goToAlertsTab, +} from '../../../tasks/rule_details'; + +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; +import { postDataView, deleteAlertsAndRules } from '../../../tasks/common'; +import { + EXCEPTION_EDIT_FLYOUT_SAVE_BTN, + EXCEPTION_ITEM_VIEWER_CONTAINER, + EXCEPTION_ITEM_CONTAINER, + FIELD_INPUT, +} from '../../../screens/exceptions'; +import { + createExceptionList, + createExceptionListItem, + deleteExceptionList, +} from '../../../tasks/api_calls/exceptions'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; +import { + addExceptionEntryFieldValueOfItemX, + addExceptionEntryFieldValueValue, +} from '../../../tasks/exceptions'; +import { ALERTS_COUNT, NUMBER_OF_ALERTS } from '../../../screens/alerts'; + +describe('Edit exception using data views from rule details', () => { + const exceptionList = getExceptionList(); + const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; + + before(() => { + esArchiverResetKibana(); + esArchiverLoad('exceptions'); + login(); + postDataView('exceptions-*'); + }); + + after(() => { + esArchiverUnload('exceptions'); + }); + + beforeEach(() => { + deleteAlertsAndRules(); + deleteExceptionList(exceptionList.list_id, exceptionList.namespace_type); + // create rule with exceptions + createExceptionList(exceptionList, exceptionList.list_id).then((response) => { + createCustomRuleEnabled( + { + ...getNewRule(), + customQuery: 'agent.name:*', + dataSource: { dataView: 'exceptions-*', type: 'dataView' }, + exceptionLists: [ + { + id: response.body.id, + list_id: exceptionList.list_id, + type: exceptionList.type, + namespace_type: exceptionList.namespace_type, + }, + ], + }, + '2', + '2s' + ); + createExceptionListItem(exceptionList.list_id, { + list_id: exceptionList.list_id, + item_id: 'simple_list_item', + tags: [], + type: 'simple', + description: 'Test exception item', + name: 'Sample Exception List Item', + namespace_type: 'single', + entries: [ + { + field: 'unique_value.test', + operator: 'included', + type: 'match_any', + value: ['bar'], + }, + ], + }); + }); + + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + goToExceptionsTab(); + }); + + afterEach(() => { + esArchiverUnload('exceptions_2'); + }); + + it('Edits an exception item', () => { + // displays existing exception item + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + + editException(); + + // check that the existing item's field is being populated + cy.get(EXCEPTION_ITEM_CONTAINER) + .eq(0) + .find(FIELD_INPUT) + .eq(0) + .should('have.text', 'unique_value.test'); + + // check that you can select a different field + addExceptionEntryFieldValueOfItemX('agent.name{downarrow}{enter}', 0, 0); + addExceptionEntryFieldValueValue('foo', 0); + + cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).click(); + cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('have.attr', 'disabled'); + cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('not.exist'); + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + + // Alerts table should still show single alert + goToAlertsTab(); + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + + // load more docs + esArchiverLoad('exceptions_2'); + + // now that 2 more docs have been added, one should match the edited exception + goToAlertsTab(); + goToOpenedAlerts(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(2); + + // there should be 2 alerts, one is the original alert and the second is for the newly + // matching doc + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', '2 alerts'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/read_only_view.spect.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/read_only_view.spect.ts new file mode 100644 index 0000000000000..f772ccad1de7b --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/read_only_view.spect.ts @@ -0,0 +1,108 @@ +/* + * 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 { getExceptionList } from '../../../objects/exception'; +import { getNewRule } from '../../../objects/rule'; +import { ROLES } from '../../../../common/test'; +import { createCustomRule } from '../../../tasks/api_calls/rules'; +import { esArchiverResetKibana } from '../../../tasks/es_archiver'; +import { login, visitWithoutDateRange } from '../../../tasks/login'; +import { goToExceptionsTab, goToAlertsTab } from '../../../tasks/rule_details'; +import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; +import { deleteAlertsAndRules } from '../../../tasks/common'; +import { + NO_EXCEPTIONS_EXIST_PROMPT, + EXCEPTION_ITEM_VIEWER_CONTAINER, + ADD_EXCEPTIONS_BTN_FROM_VIEWER_HEADER, + ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN, +} from '../../../screens/exceptions'; +import { EXCEPTION_ITEM_ACTIONS_BUTTON } from '../../../screens/rule_details'; +import { + createExceptionList, + createExceptionListItem, + deleteExceptionList, +} from '../../../tasks/api_calls/exceptions'; + +describe('Exceptions viewer read only', () => { + const exceptionList = getExceptionList(); + const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; + + before(() => { + esArchiverResetKibana(); + // create rule with exceptions + createExceptionList(exceptionList, exceptionList.list_id).then((response) => { + createCustomRule( + { + ...getNewRule(), + customQuery: 'agent.name:*', + dataSource: { index: ['exceptions*'], type: 'indexPatterns' }, + exceptionLists: [ + { + id: response.body.id, + list_id: exceptionList.list_id, + type: exceptionList.type, + namespace_type: exceptionList.namespace_type, + }, + ], + }, + '2' + ); + }); + + login(ROLES.reader); + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL, ROLES.reader); + goToRuleDetails(); + goToExceptionsTab(); + }); + + after(() => { + deleteAlertsAndRules(); + deleteExceptionList(exceptionList.list_id, exceptionList.namespace_type); + }); + + it('Cannot add an exception from empty viewer screen', () => { + // when no exceptions exist, empty component shows with action to add exception + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); + + // cannot add an exception from empty view + cy.get(ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN).should('have.attr', 'disabled'); + }); + + it('Cannot take actions on exception', () => { + createExceptionListItem(exceptionList.list_id, { + list_id: exceptionList.list_id, + item_id: 'simple_list_item', + tags: [], + type: 'simple', + description: 'Test exception item', + name: 'Sample Exception List Item', + namespace_type: 'single', + entries: [ + { + field: 'unique_value.test', + operator: 'included', + type: 'match_any', + value: ['bar'], + }, + ], + }); + + goToAlertsTab(); + goToExceptionsTab(); + + // can view exceptions + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + + // cannot access edit/delete actions of item + cy.get(EXCEPTION_ITEM_ACTIONS_BUTTON).should('have.attr', 'disabled'); + + // does not display add exception button + cy.get(ADD_EXCEPTIONS_BTN_FROM_VIEWER_HEADER).should('not.exist'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts index 56dcfdbf97ead..1ca8ded946300 100644 --- a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts @@ -5,9 +5,6 @@ * 2.0. */ -export const ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN = - '[data-test-subj="exceptionsEmptyPromptButton"]'; - export const CLOSE_ALERTS_CHECKBOX = '[data-test-subj="bulk-close-alert-on-add-add-exception-checkbox"]'; @@ -68,3 +65,20 @@ export const EXCEPTION_FLYOUT_VERSION_CONFLICT = '[data-test-subj="exceptionsFlyoutVersionConflict"]'; export const EXCEPTION_FLYOUT_LIST_DELETED_ERROR = '[data-test-subj="errorCalloutContainer"]'; + +// Exceptions all items view +export const NO_EXCEPTIONS_EXIST_PROMPT = + '[data-test-subj="exceptionItemViewerEmptyPrompts-empty-detection"]'; + +export const ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN = + '[data-test-subj="exceptionsEmptyPromptButton"]'; + +export const EXCEPTION_ITEM_VIEWER_CONTAINER = '[data-test-subj="exceptionItemContainer"]'; + +export const ADD_EXCEPTIONS_BTN_FROM_VIEWER_HEADER = + '[data-test-subj="exceptionsHeaderAddExceptionBtn"]'; + +export const NO_EXCEPTIONS_SEARCH_RESULTS_PROMPT = + '[data-test-subj="exceptionItemViewerEmptyPrompts-emptySearch"]'; + +export const EXCEPTION_ITEM_VIEWER_SEARCH = 'input[data-test-subj="exceptionsViewerSearchBar"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index 989353cf7a253..80883d825c18f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -31,7 +31,7 @@ export const DETAILS_DESCRIPTION = '.euiDescriptionList__description'; export const DETAILS_TITLE = '.euiDescriptionList__title'; -export const EXCEPTIONS_TAB = '[data-test-subj="navigation-rule_exceptions"]'; +export const EXCEPTIONS_TAB = 'a[data-test-subj="navigation-rule_exceptions"]'; export const FALSE_POSITIVES_DETAILS = 'False positive examples'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index cb2fc257f7dc7..afee74e1a943b 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -218,6 +218,7 @@ export const createCustomRuleEnabled = ( query: rule.customQuery, language: 'kuery', enabled: true, + exceptions_list: rule.exceptionLists ?? [], tags: ['rule1'], max_signals: maxSignals, building_block_type: rule.buildingBlockType, @@ -243,6 +244,7 @@ export const createCustomRuleEnabled = ( query: rule.customQuery, language: 'kuery', enabled: true, + exceptions_list: rule.exceptionLists ?? [], tags: ['rule1'], max_signals: maxSignals, building_block_type: rule.buildingBlockType, diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index 21fc23a1a72fa..b4c70d709b3ec 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -9,8 +9,10 @@ import type { Exception } from '../objects/exception'; import { RULE_STATUS } from '../screens/create_new_rule'; import { ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN, + ADD_EXCEPTIONS_BTN_FROM_VIEWER_HEADER, CLOSE_ALERTS_CHECKBOX, CONFIRM_BTN, + EXCEPTION_ITEM_VIEWER_SEARCH, FIELD_INPUT, LOADING_SPINNER, OPERATOR_INPUT, @@ -65,7 +67,7 @@ export const addsFieldsToTimeline = (search: string, fields: string[]) => { closeFieldsBrowser(); }; -export const openExceptionFlyoutFromRuleSettings = () => { +export const openExceptionFlyoutFromEmptyViewerPrompt = () => { cy.root() .pipe(($el) => { $el.find(ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN).trigger('click'); @@ -74,8 +76,34 @@ export const openExceptionFlyoutFromRuleSettings = () => { .should('be.visible'); }; -export const addsExceptionFromRuleSettings = (exception: Exception) => { - openExceptionFlyoutFromRuleSettings(); +export const searchForExceptionItem = (query: string) => { + cy.get(EXCEPTION_ITEM_VIEWER_SEARCH).type(`${query}{enter}`).trigger('change'); +}; + +export const addExceptionFlyoutFromViewerHeader = () => { + cy.root() + .pipe(($el) => { + $el.find(ADD_EXCEPTIONS_BTN_FROM_VIEWER_HEADER).trigger('click'); + return $el.find(FIELD_INPUT); + }) + .should('be.visible'); +}; + +export const addExceptionFromRuleDetails = (exception: Exception) => { + addExceptionFlyoutFromViewerHeader(); + cy.get(FIELD_INPUT).type(`${exception.field}{downArrow}{enter}`); + cy.get(OPERATOR_INPUT).type(`${exception.operator}{enter}`); + exception.values.forEach((value) => { + cy.get(VALUES_INPUT).type(`${value}{enter}`); + }); + cy.get(CLOSE_ALERTS_CHECKBOX).click({ force: true }); + cy.get(CONFIRM_BTN).click(); + cy.get(CONFIRM_BTN).should('have.attr', 'disabled'); + cy.get(CONFIRM_BTN).should('not.exist'); +}; + +export const addFirstExceptionFromRuleDetails = (exception: Exception) => { + openExceptionFlyoutFromEmptyViewerPrompt(); cy.get(FIELD_INPUT).type(`${exception.field}{downArrow}{enter}`); cy.get(OPERATOR_INPUT).type(`${exception.operator}{enter}`); exception.values.forEach((value) => { @@ -92,13 +120,14 @@ export const goToAlertsTab = () => { }; export const goToExceptionsTab = () => { + cy.get(EXCEPTIONS_TAB).should('exist'); cy.get(EXCEPTIONS_TAB).click(); }; export const editException = () => { - cy.get(EXCEPTION_ITEM_ACTIONS_BUTTON).click({ force: true }); + cy.get(EXCEPTION_ITEM_ACTIONS_BUTTON).eq(0).click({ force: true }); - cy.get(EDIT_EXCEPTION_BTN).click({ force: true }); + cy.get(EDIT_EXCEPTION_BTN).eq(0).click({ force: true }); }; export const removeException = () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.tsx index f4dd31509e3dc..fdffa134dd96f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.tsx @@ -28,6 +28,7 @@ const MyFlexItem = styled(EuiFlexItem)` `; interface ExceptionItemsViewerProps { + isReadOnly: boolean; disableActions: boolean; exceptions: ExceptionListItemSchema[]; listType: ExceptionListTypeEnum; @@ -39,6 +40,7 @@ interface ExceptionItemsViewerProps { } const ExceptionItemsViewerComponent: React.FC = ({ + isReadOnly, exceptions, listType, disableActions, @@ -52,6 +54,7 @@ const ExceptionItemsViewerComponent: React.FC = ({ <> {viewerState != null && viewerState !== 'deleting' ? ( { it('it renders loading screen when "currentState" is "loading"', () => { const wrapper = mount( { it('it renders empty search screen when "currentState" is "empty_search"', () => { const wrapper = mount( { it('it renders no endpoint items screen when "currentState" is "empty" and "listType" is "endpoint"', () => { const wrapper = mount( { it('it renders no exception items screen when "currentState" is "empty" and "listType" is "detection"', () => { const wrapper = mount( void; } const ExeptionItemsViewerEmptyPromptsComponent = ({ + isReadOnly, listType, currentState, onCreateExceptionListItem, @@ -69,6 +71,7 @@ const ExeptionItemsViewerEmptyPromptsComponent = ({ onClick={onCreateExceptionListItem} iconType="plusInCircle" color="primary" + isDisabled={isReadOnly} fill > {listType === ExceptionListTypeEnum.ENDPOINT @@ -97,7 +100,7 @@ const ExeptionItemsViewerEmptyPromptsComponent = ({ ); } - }, [currentState, euiTheme.colors.darkestShade, listType, onCreateExceptionListItem]); + }, [currentState, euiTheme.colors.darkestShade, isReadOnly, listType, onCreateExceptionListItem]); return ( { wrapper.find('button[data-test-subj="tablePaginationPopoverButton"]').at(0).simulate('click'); wrapper.find('button[data-test-subj="tablePagination-50-rows"]').at(0).simulate('click'); - expect(mockOnPaginationChange).toHaveBeenCalledWith({ page: 0, perPage: 50 }); + expect(mockOnPaginationChange).toHaveBeenCalledWith({ pagination: { page: 0, perPage: 50 } }); }); it('it invokes "onPaginationChange" when next clicked', () => { @@ -47,7 +47,7 @@ describe('ExceptionsViewerPagination', () => { wrapper.find('button[data-test-subj="pagination-button-next"]').at(0).simulate('click'); - expect(mockOnPaginationChange).toHaveBeenCalledWith({ page: 1, perPage: 5 }); + expect(mockOnPaginationChange).toHaveBeenCalledWith({ pagination: { page: 1, perPage: 5 } }); }); it('it invokes "onPaginationChange" when page clicked', () => { @@ -66,6 +66,6 @@ describe('ExceptionsViewerPagination', () => { wrapper.find('button[data-test-subj="pagination-button-2"]').simulate('click'); - expect(mockOnPaginationChange).toHaveBeenCalledWith({ page: 2, perPage: 50 }); + expect(mockOnPaginationChange).toHaveBeenCalledWith({ pagination: { page: 2, perPage: 50 } }); }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx index 20133d54b94d5..2518ca27cf15f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx @@ -64,7 +64,7 @@ const ExceptionsViewerSearchBarComponent = ({ }: ExceptionsViewerSearchBarProps): JSX.Element => { const handleOnSearch = useCallback( ({ queryText }): void => { - onSearch(queryText); + onSearch({ search: queryText }); }, [onSearch] ); @@ -87,6 +87,7 @@ const ExceptionsViewerSearchBarComponent = ({ placeholder: 'Search on the fields below: e.g. name:"my list"', incremental: false, schema: ITEMS_SCHEMA, + 'data-test-subj': 'exceptionsViewerSearchBar', }} filters={[]} onChange={handleOnSearch} diff --git a/x-pack/test/security_solution_cypress/es_archives/exceptions_2/data.json b/x-pack/test/security_solution_cypress/es_archives/exceptions_2/data.json index 3ad636b20a9cc..72dc01b9bac54 100644 --- a/x-pack/test/security_solution_cypress/es_archives/exceptions_2/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/exceptions_2/data.json @@ -4,7 +4,7 @@ "id": "_aZE5nwBOpWiDweSth_E", "index": "exceptions-0002", "source": { - "@timestamp": "2019-09-02T00:41:06.527Z", + "@timestamp": "2019-09-02T00:45:06.527Z", "agent": { "name": "foo" }, @@ -23,4 +23,31 @@ ] } } -} \ No newline at end of file +} + +{ + "type": "doc", + "value": { + "id": "_aZE5nwBOpWiDweSth_F", + "index": "exceptions-0002", + "source": { + "@timestamp": "2019-09-02T00:46:06.527Z", + "agent": { + "name": "bar" + }, + "unique_value": { + "test": "test field 2" + }, + "user" : [ + { + "name" : "foo", + "id" : "123" + }, + { + "name" : "bar", + "id" : "456" + } + ] + } + } +} From af121eb7016a2e9128c11d1041facfcb52744a47 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Fri, 2 Sep 2022 16:48:15 -0700 Subject: [PATCH 28/30] making references route internal --- .../security_solution/common/constants.ts | 3 +++ .../alerts_table_flow/add_exception.spec.ts | 2 +- .../cypress/tasks/rule_details.ts | 7 +++++- .../all_items.test.tsx | 3 +++ .../all_exception_items_table/index.tsx | 8 ++----- .../all_exception_items_table/pagination.tsx | 2 +- .../search_bar.test.tsx | 6 ++--- .../all_exception_items_table/search_bar.tsx | 6 ++--- .../detection_engine/rules/api.test.ts | 22 +++++++++---------- .../containers/detection_engine/rules/api.ts | 3 ++- .../rules/find_rule_exceptions_route.test.ts | 10 ++++----- .../rules/find_rule_exceptions_route.ts | 8 +++---- .../group1/find_rule_exception_references.ts | 8 +++---- 13 files changed, 47 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index e86e57e00ca34..6f3958cbb54e1 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -290,10 +290,13 @@ export const prebuiltSavedObjectsBulkCreateUrl = (templateName: string) => * Internal detection engine routes */ export const INTERNAL_DETECTION_ENGINE_URL = '/internal/detection_engine' as const; +export const INTERNAL_DETECTION_ENGINE_RULES_URL = '/internal/detection_engine/rules' as const; export const DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL = `${INTERNAL_DETECTION_ENGINE_URL}/fleet/integrations/installed` as const; export const DETECTION_ENGINE_ALERTS_INDEX_URL = `${INTERNAL_DETECTION_ENGINE_URL}/signal/index` as const; +export const DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL = + `${INTERNAL_DETECTION_ENGINE_RULES_URL}/exceptions/_find_references` as const; /** * Telemetry detection endpoint for any previews requested of what data we are * providing through UI/UX and for e2e tests. diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_table_flow/add_exception.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_table_flow/add_exception.spec.ts index 58e58b3ba2f83..f1d6d2f1cc063 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_table_flow/add_exception.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_table_flow/add_exception.spec.ts @@ -97,6 +97,6 @@ describe('Adds rule exception from alerts flow', () => { waitForAlertsToPopulate(); cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + cy.get(NUMBER_OF_ALERTS).should('have.text', '2 alerts'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index b4c70d709b3ec..1cec924eae55a 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -77,7 +77,12 @@ export const openExceptionFlyoutFromEmptyViewerPrompt = () => { }; export const searchForExceptionItem = (query: string) => { - cy.get(EXCEPTION_ITEM_VIEWER_SEARCH).type(`${query}{enter}`).trigger('change'); + cy.get(EXCEPTION_ITEM_VIEWER_SEARCH).type(`${query}`).trigger('keydown', { + key: 'Enter', + keyCode: 13, + code: 'Enter', + type: 'keydown', + }); }; export const addExceptionFlyoutFromViewerHeader = () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.test.tsx index 7897b930bbd86..0df9fad55a14d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.test.tsx @@ -37,6 +37,7 @@ describe('ExceptionsViewerItems', () => { onCreateExceptionListItem={jest.fn()} onDeleteException={jest.fn()} onEditExceptionItem={jest.fn()} + isReadOnly={false} /> ); @@ -60,6 +61,7 @@ describe('ExceptionsViewerItems', () => { onCreateExceptionListItem={jest.fn()} onDeleteException={jest.fn()} onEditExceptionItem={jest.fn()} + isReadOnly={false} /> @@ -84,6 +86,7 @@ describe('ExceptionsViewerItems', () => { onCreateExceptionListItem={jest.fn()} onDeleteException={jest.fn()} onEditExceptionItem={jest.fn()} + isReadOnly={false} /> diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx index f0a9e37320b07..e6168bb39edbd 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx @@ -301,11 +301,7 @@ const ExceptionsViewerComponent = ({ // User privileges checks useEffect((): void => { - if (!canUserCRUD || !hasIndexWrite) { - setReadOnly(true); - } else { - setReadOnly(false); - } + setReadOnly(!canUserCRUD || !hasIndexWrite); }, [setReadOnly, canUserCRUD, hasIndexWrite]); useEffect(() => { @@ -351,7 +347,7 @@ const ExceptionsViewerComponent = ({ <> {!STATES_SEARCH_HIDDEN.includes(viewerState) && ( { onSearch={jest.fn()} onAddExceptionClick={jest.fn()} isSearching={false} - isReadOnly + canAddException /> ); @@ -31,7 +31,7 @@ describe('ExceptionsViewerSearchBar', () => { const mockOnAddExceptionClick = jest.fn(); const wrapper = mount( { const mockOnAddExceptionClick = jest.fn(); const wrapper = mount( - {!isReadOnly && ( + {!canAddException && ( { }, ]; await findRuleExceptionReferences({ lists: payload, signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith( - '/api/detection_engine/rules/exceptions/_find_references', - { - query: { - ids: '123,456', - list_ids: 'list_id_1,list_id_2', - namespace_types: 'single,single', - }, - method: 'GET', - signal: abortCtrl.signal, - } - ); + expect(fetchMock).toHaveBeenCalledWith(DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL, { + query: { + ids: '123,456', + list_ids: 'list_id_1,list_id_2', + namespace_types: 'single,single', + }, + method: 'GET', + signal: abortCtrl.signal, + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index d0df637e71388..ae354aa993d29 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -17,6 +17,7 @@ import { DETECTION_ENGINE_RULES_PREVIEW, DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL, DETECTION_ENGINE_RULES_URL_FIND, + DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL, } from '../../../../../common/constants'; import type { BulkAction } from '../../../../../common/detection_engine/schemas/common'; import type { @@ -385,7 +386,7 @@ export const findRuleExceptionReferences = async ({ signal, }: FindRulesReferencedByExceptionsProps): Promise => KibanaServices.get().http.fetch( - `${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, + DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL, { method: 'GET', query: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.test.ts index 9287cd782518d..1ca0fd4dabf60 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL } from '../../../../../common/constants'; import { getEmptyFindResult, getFindResultWithSingleHit, @@ -49,7 +49,7 @@ describe('findRuleExceptionReferencesRoute', () => { test('returns 200 when adding an exception item and rule_default exception list already exists', async () => { const request = requestMock.create({ method: 'get', - path: `${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references?exception_list`, + path: `${DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL}?exception_list`, query: { ids: `4656dc92-5832-11ea-8e2d-0242ac130003`, list_ids: `my_default_list`, @@ -85,7 +85,7 @@ describe('findRuleExceptionReferencesRoute', () => { test('returns 200 when no references found', async () => { const request = requestMock.create({ method: 'get', - path: `${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, + path: DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL, query: { ids: `4656dc92-5832-11ea-8e2d-0242ac130003`, list_ids: `my_default_list`, @@ -112,7 +112,7 @@ describe('findRuleExceptionReferencesRoute', () => { test('returns 400 if query param lengths do not match', async () => { const request = requestMock.create({ method: 'get', - path: `${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, + path: DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL, query: { ids: `4656dc92-5832-11ea-8e2d-0242ac130003`, list_ids: `my_default_list`, @@ -135,7 +135,7 @@ describe('findRuleExceptionReferencesRoute', () => { test('returns 500 if rules client fails', async () => { const request = requestMock.create({ method: 'get', - path: `${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, + path: DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL, query: { ids: `4656dc92-5832-11ea-8e2d-0242ac130003`, list_ids: `my_default_list`, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts index e0e9caf4d0ba8..8ac258322e260 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts @@ -11,7 +11,7 @@ import { validate } from '@kbn/securitysolution-io-ts-utils'; import type { FindResult } from '@kbn/alerting-plugin/server'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL } from '../../../../../common/constants'; import { buildSiemResponse } from '../utils'; import { enrichFilterWithRuleTypeMapping } from '../../rules/enrich_filter_with_rule_type_mappings'; import type { FindExceptionReferencesOnRuleSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/find_exception_list_references_schema'; @@ -24,7 +24,7 @@ import type { RuleParams } from '../../schemas/rule_schemas'; export const findRuleExceptionReferencesRoute = (router: SecuritySolutionPluginRouter) => { router.get( { - path: `${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`, + path: DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL, validate: { query: buildRouteValidation< typeof findExceptionReferencesOnRuleSchema, @@ -51,7 +51,7 @@ export const findRuleExceptionReferencesRoute = (router: SecuritySolutionPluginR }); } - const results: Array> = await Promise.all( + const foundRules: Array> = await Promise.all( ids.map(async (id, index) => { return rulesClient.find({ options: { @@ -66,7 +66,7 @@ export const findRuleExceptionReferencesRoute = (router: SecuritySolutionPluginR }) ); - const references = results.map(({ data }, index) => { + const references = foundRules.map(({ data }, index) => { const wantedData = data.map(({ name, id, params }) => ({ name, id, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rule_exception_references.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rule_exception_references.ts index f6ea0f92970db..e75a35d88acc3 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rule_exception_references.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rule_exception_references.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL } from '@kbn/security-solution-plugin/common/constants'; import { CreateExceptionListSchema, ExceptionListTypeEnum, @@ -57,7 +57,7 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, getSimpleRule('rule-1')); const { body: references } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`) + .get(DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL) .set('kbn-xsrf', 'true') .query({ ids: `${exceptionList.id}`, @@ -74,7 +74,7 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, getSimpleRule('rule-1')); const { body: references } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`) + .get(DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL) .set('kbn-xsrf', 'true') .query({ ids: `1234`, @@ -120,7 +120,7 @@ export default ({ getService }: FtrProviderContext) => { }); const { body: references } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}/exceptions/_find_references`) + .get(DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL) .set('kbn-xsrf', 'true') .query({ ids: `${exceptionList.id},${exceptionList2.id}`, From 932cbbc7ba552008501d58765b501e91280cc866 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Sat, 3 Sep 2022 13:13:54 -0700 Subject: [PATCH 29/30] fix check type error --- .../exceptions/rule_details_flow/read_only_view.spect.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/read_only_view.spect.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/read_only_view.spect.ts index f772ccad1de7b..b11c688520b1a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/read_only_view.spect.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/read_only_view.spect.ts @@ -30,7 +30,6 @@ import { describe('Exceptions viewer read only', () => { const exceptionList = getExceptionList(); - const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; before(() => { esArchiverResetKibana(); From 20f82800bd8ba4e99dbe5aca08c7cb5e1b0ce3e4 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 8 Sep 2022 10:09:00 -0700 Subject: [PATCH 30/30] fix bug found during PR testing - thanks Mike! --- .../rule_exceptions/components/exception_item_card/meta.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.tsx index 517f10d43b506..a24526dd04e3a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.tsx @@ -102,7 +102,7 @@ export const ExceptionItemCardMetaInfo = memo( iconType="list" data-test-subj={`${dataTestSubj}-affectedRulesButton`} > - {i18n.AFFECTED_RULES(references.length)} + {i18n.AFFECTED_RULES(references?.length ?? 0)} } panelPaddingSize="none"