diff --git a/packages/kbn-securitysolution-exception-list-components/README.md b/packages/kbn-securitysolution-exception-list-components/README.md index 3ebfc32b7e731..44ef2c44a64ef 100644 --- a/packages/kbn-securitysolution-exception-list-components/README.md +++ b/packages/kbn-securitysolution-exception-list-components/README.md @@ -1,7 +1,6 @@ # @kbn/securitysolution-exception-list-components This is where the building UI components of the Exception-List live - Most of the components here are imported from `x-pack/plugins/security_solutions/public/detection_engine` # Aim @@ -10,8 +9,19 @@ TODO # Pattern used -TODO +``` +component + index.tsx + index.styles.ts <-- to hold styles if the component has many custom styles + useComponent.ts <-- for logic if the Presentational Component has logic + index.test.tsx + useComponent.test.tsx +``` # Next -- Now *ExceptionItems* receives *securityLinkAnchorComponent* as props to avoid moving all the common components at once, later we should move all building blocks to the library +- Now the `ExceptionItems, ExceptionItemCard +and ExceptionItemCardMetaInfo + ` receive `securityLinkAnchorComponent, exceptionsUtilityComponent +, and exceptionsUtilityComponent +` as props to avoid moving all the `common` components under the `x-pack` at once, later we should move all building blocks to this `kbn-package` diff --git a/packages/kbn-securitysolution-exception-list-components/src/empty_viewer_state/empty_viewer_state.test.tsx b/packages/kbn-securitysolution-exception-list-components/src/empty_viewer_state/empty_viewer_state.test.tsx index 21cf66b7de6f7..43943e0e8fb97 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/empty_viewer_state/empty_viewer_state.test.tsx +++ b/packages/kbn-securitysolution-exception-list-components/src/empty_viewer_state/empty_viewer_state.test.tsx @@ -13,7 +13,7 @@ import { EmptyViewerState } from './empty_viewer_state'; import { ListTypeText, ViewerStatus } from '../types'; describe('EmptyViewerState', () => { - it('it renders error screen when "viewerStatus" is "error" with the default title and body', () => { + it('it should render "error" with the default title and body', () => { const wrapper = render( { 'There was an error loading the exception items. Contact your administrator for help.' ); }); - it('it renders error screen when "viewerStatus" is "error" when sending the title and body props', () => { + it('it should render "error" when sending the title and body props', () => { const wrapper = render( { expect(wrapper.getByTestId('errorTitle')).toHaveTextContent('Error title'); expect(wrapper.getByTestId('errorBody')).toHaveTextContent('Error body'); }); - it('it renders loading screen when "viewerStatus" is "loading"', () => { + it('it should render loading', () => { const wrapper = render( { expect(wrapper.getByTestId('loadingViewerState')).toBeTruthy(); }); - it('it renders empty search screen when "viewerStatus" is "empty_search" with the default title and body', () => { + it('it should render empty search with the default title and body', () => { const wrapper = render( { ); expect(wrapper.getByTestId('emptySearchBody')).toHaveTextContent('Try modifying your search'); }); - it('it renders empty search screen when "viewerStatus" is "empty_search" when sending title and body props', () => { + it('it should render empty search when sending title and body props', () => { const wrapper = render( { expect(wrapper.getByTestId('emptySearchTitle')).toHaveTextContent('Empty search title'); expect(wrapper.getByTestId('emptySearchBody')).toHaveTextContent('Empty search body'); }); - it('it renders no items screen when "viewerStatus" is "empty" when sending title and body props', () => { + it('it should render no items screen when sending title and body props', () => { const wrapper = render( { expect(getByTestId('emptyStateButton')).toHaveTextContent('Add endpoint exception'); expect(getByTestId('emptyViewerState')).toBeTruthy(); }); - it('it renders no items screen when "viewerStatus" is "empty" with default title and body props', () => { + it('it should render no items with default title and body props', () => { const wrapper = render( { ); expect(getByTestId('emptyStateButton')).toHaveTextContent('Create rule exception'); }); - it('it renders no items screen when "viewerStatus" is "empty" with default title and body props and listType endPoint', () => { + it('it should render no items screen with default title and body props and listType endPoint', () => { const wrapper = render( { - it('temp', () => { - expect(true).toEqual(true); - }); -}); - -// import { render } from '@testing-library/react'; -// import React from 'react'; - -// // import { TestProviders } from '../../../../../../common/mock'; -// import { ExceptionItemCardConditions } from './conditions'; - -// interface TestEntry { -// field: string; -// operator: 'included' | 'excluded'; -// type: unknown; -// value?: string; -// entries?: TestEntry[]; -// } -// const getEntryKey = (entry: TestEntry, index: string) => { -// const { field, type, value } = entry; -// return `${field}${type}${value || ''}${index}`; -// }; -// const entries: TestEntry[] = [ -// { -// field: 'host.name', -// operator: 'included', -// type: 'match', -// value: 'host', -// }, -// { -// field: 'threat.indicator.port', -// operator: 'included', -// type: 'exists', -// }, -// { -// entries: [ -// { -// field: 'valid', -// operator: 'included', -// type: 'match', -// value: 'true', -// }, -// ], -// field: 'file.Ext.code_signature', -// type: 'nested', -// operator: 'included', -// }, -// ]; -// describe('ExceptionItemCardConditions', () => { -// beforeEach(() => { -// jest.clearAllMocks(); -// jest.resetAllMocks(); -// }); -// it('it includes os condition if one exists', () => { -// const wrapper = render( -// // -// -// // -// ); -// // Text is gonna look a bit off unformatted - -// expect(wrapper.getByTestId('exceptionItemConditions-os')).toHaveTextContent('OSIS Linux'); - -// const testId = `exceptionItemConditions-${getEntryKey(entries[0], '0')}-condition`; -// expect(wrapper.getByTestId(testId)).toHaveTextContent('host.nameIS host'); +import { render } from '@testing-library/react'; +import React from 'react'; -// const testId1 = `exceptionItemConditions-${getEntryKey(entries[1], '1')}-condition`; -// expect(wrapper.getByTestId(testId1)).toHaveTextContent('AND threat.indicator.portexists'); +import { ExceptionItemCardConditions } from './conditions'; -// const testId2 = `exceptionItemConditions-${getEntryKey(entries[2], '2')}-condition`; -// expect(wrapper.getByTestId(testId2)).toHaveTextContent('AND file.Ext.code_signature'); +interface TestEntry { + field: string; + operator: 'included' | 'excluded'; + type: unknown; + value?: string | string[]; + entries?: TestEntry[]; + list?: { id: string; type: string }; +} +const getEntryKey = ( + entry: TestEntry, + index: string, + list?: { id: string; type: string } | null +) => { + if (list && Object.keys(list)) { + const { field, type, list: entryList } = entry; + const { id } = entryList || {}; + return `${field}${type}${id || ''}${index}`; + } + const { field, type, value } = entry; + return `${field}${type}${value || ''}${index}`; +}; -// // const testId2Nested = `nested-${getEntryKey(entries[2]?.entries[0], '0')}-condition`; -// // expect(wrapper.getByTestId(testId2Nested)).toHaveTextContent('validIS true'); -// }); - -// // TODO: FIX test and add more -// // it('it renders item conditions', () => { -// // const wrapper = render( -// // -// // -// // -// // ); +describe('ExceptionItemCardConditions', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + it('it includes os condition if one exists', () => { + const entries: TestEntry[] = [ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'host', + }, + { + field: 'threat.indicator.port', + operator: 'included', + type: 'exists', + }, + { + entries: [ + { + field: 'valid', + operator: 'included', + type: 'match', + value: 'true', + }, + ], + field: 'file.Ext.code_signature', + type: 'nested', + operator: 'included', + }, + ]; + const wrapper = render( + + ); + expect(wrapper.getByTestId('exceptionItemConditionsOs')).toHaveTextContent('OSIS Linux'); -// // expect(wrapper.queryByTestId('exceptionItemConditions-os')).not.toBeInTheDocument(); -// // const conditions = wrapper.getAllByTestId('exceptionItemConditions-condition'); -// // expect(conditions[0]).toHaveTextContent('host.nameIS host'); + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[0], '0')}EntryContent`) + ).toHaveTextContent('host.nameIS host'); -// // // Match -// // expect(conditions[1]).toHaveTextContent('AND host.nameIS NOT host'); -// // // MATCH_ANY -// // expect(conditions[2]).toHaveTextContent('AND host.nameis one of foobar'); -// // expect(conditions[3]).toHaveTextContent('AND host.nameis not one of foobar'); + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[1], '1')}EntryContent`) + ).toHaveTextContent('AND threat.indicator.portexists'); -// // // WILDCARD -// // expect(conditions[4]).toHaveTextContent('AND user.nameMATCHES foo*'); -// // expect(conditions[5]).toHaveTextContent('AND user.nameDOES NOT MATCH foo*'); + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[2], '2')}EntryContent`) + ).toHaveTextContent('AND file.Ext.code_signature'); -// // // EXISTS -// // expect(conditions[6]).toHaveTextContent('AND threat.indicator.portexists'); -// // expect(conditions[7]).toHaveTextContent('AND threat.indicator.portdoes not exist'); + if (entries[2] && entries[2].entries) { + expect( + wrapper.getByTestId( + `exceptionItemConditions${getEntryKey(entries[2].entries[0], '0')}EntryContent` + ) + ).toHaveTextContent('validIS true'); + } + }); -// // // NESTED -// // expect(conditions[8]).toHaveTextContent('AND file.Ext.code_signature validIS true'); -// // }); + it('it renders item conditions', () => { + const entries: TestEntry[] = [ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'host', + }, + { + field: 'host.name', + operator: 'excluded', + type: 'match', + value: 'host', + }, + { + field: 'host.name', + operator: 'included', + type: 'match_any', + value: ['foo', 'bar'], + }, + { + field: 'host.name', + operator: 'excluded', + type: 'match_any', + value: ['foo', 'bar'], + }, + { + field: 'user.name', + operator: 'included', + type: 'wildcard', + value: 'foo*', + }, + { + field: 'user.name', + operator: 'excluded', + type: 'wildcard', + value: 'foo*', + }, + { + field: 'threat.indicator.port', + operator: 'included', + type: 'exists', + }, + { + field: 'threat.indicator.port', + operator: 'excluded', + type: 'exists', + }, + { + entries: [ + { + field: 'valid', + operator: 'included', + type: 'match', + value: 'true', + }, + ], + field: 'file.Ext.code_signature', + type: 'nested', + operator: 'included', + }, + ]; + const wrapper = render( + + ); + expect(wrapper.queryByTestId('exceptionItemConditionsOs')).not.toBeInTheDocument(); -// // it('it renders list conditions', () => { -// // const wrapper = render( -// // -// // -// // -// // ); + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[0], '0')}EntryContent`) + ).toHaveTextContent('host.nameIS host'); + // Match; + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[1], '1')}EntryContent`) + ).toHaveTextContent('AND host.nameIS NOT host'); + // MATCH_ANY; + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[2], '2')}EntryContent`) + ).toHaveTextContent('AND host.nameis one of foobar'); + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[3], '3')}EntryContent`) + ).toHaveTextContent('AND host.nameis not one of foobar'); + // WILDCARD; + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[4], '4')}EntryContent`) + ).toHaveTextContent('AND user.nameMATCHES foo*'); + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[5], '5')}EntryContent`) + ).toHaveTextContent('AND user.nameDOES NOT MATCH foo*'); + // EXISTS; + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[6], '6')}EntryContent`) + ).toHaveTextContent('AND threat.indicator.portexists'); + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[7], '7')}EntryContent`) + ).toHaveTextContent('AND threat.indicator.portdoes not exist'); + // NESTED; + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[8], '8')}EntryContent`) + ).toHaveTextContent('AND file.Ext.code_signature'); + if (entries[8] && entries[8].entries) { + expect( + wrapper.getByTestId( + `exceptionItemConditions${getEntryKey(entries[8].entries[0], '0')}EntryContent` + ) + ).toHaveTextContent('validIS true'); + } + }); + it('it renders list conditions', () => { + const entries: TestEntry[] = [ + { + field: 'host.name', + list: { + id: 'ips.txt', + type: 'keyword', + }, + operator: 'included', + type: 'list', + }, + { + field: 'host.name', + list: { + id: 'ips.txt', + type: 'keyword', + }, + operator: 'excluded', + type: 'list', + }, + ]; + const wrapper = render( + + ); + // /exceptionItemConditionshost.namelist0EntryContent + expect( + wrapper.getByTestId( + `exceptionItemConditions${getEntryKey(entries[0], '0', entries[0].list)}EntryContent` + ) + ).toHaveTextContent('host.nameincluded in ips.txt'); -// // // Text is gonna look a bit off unformatted -// // const conditions = wrapper.getAllByTestId('exceptionItemConditions-condition'); -// // expect(conditions[0]).toHaveTextContent('host.nameincluded in ips.txt'); -// // expect(conditions[1]).toHaveTextContent('ND host.nameis not included in ips.txt'); -// // }); -// }); + expect( + wrapper.getByTestId( + `exceptionItemConditions${getEntryKey(entries[1], '1', entries[1].list)}EntryContent` + ) + ).toHaveTextContent('AND host.nameis not included in ips.txt'); + }); +}); diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.tsx index f145441fe2593..8b85a7343afc1 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.tsx +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.tsx @@ -28,9 +28,9 @@ export const ExceptionItemCardConditions = memo( {entries.map((entry: Entry, index: number) => { const nestedEntries = 'entries' in entry ? entry.entries : []; return ( -
+
( {nestedEntries?.length ? nestedEntries.map((nestedEntry: Entry, nestedIndex: number) => ( +
{isNestedEntry ? ( (({ os, dataTestSubj }) => { return os.map((osValue) => OS_LABELS[osValue] ?? osValue).join(', '); }, [os]); return osLabel ? ( -
+