From 0a5ffb5ce274e61cf9a17f1e0c2404577848cccc Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Tue, 12 Nov 2024 12:11:16 -0500 Subject: [PATCH] [Security Solution][Endpoint Exceptions] Warning callout for incomplete code signature for endpoint exceptions (#198245) ## Summary Navigate to Security Solution > Manage > Rules > Add Endpoint Exception - [x] Warning callout shown in endpoint exceptions when code signature field is incomplete (i.e. process.code_signature.subject_name w/o process.code_signature.trusted or vice versa) - [x] For mac operating systems, process.code_signature.team_id is also accepted as an equivalent to subject_name - [ ] Warning callout is also shown for nested entries for this code signature field: process.Ext.code_signature - [x] Unit Tests # Screenshots Subject name only -- warning is present ![image](https://github.com/user-attachments/assets/eccf4d49-a4b1-47fc-8c51-bddf4fd6664f) Trusted field only -- warning is present ![image](https://github.com/user-attachments/assets/d3ba6716-e7d1-4709-a5b1-1e472964b6e3) Both subject name and trusted fields -- no warning is present ![image](https://github.com/user-attachments/assets/11b179ff-278e-4ec6-a749-638f428215aa) --- .../add_exception_flyout/index.test.tsx | 176 ++++++++++++------ .../components/add_exception_flyout/index.tsx | 16 +- .../add_exception_flyout/reducer.ts | 15 +- .../edit_exception_flyout/index.test.tsx | 52 ++++++ .../edit_exception_flyout/index.tsx | 17 +- .../edit_exception_flyout/reducer.ts | 12 ++ 6 files changed, 230 insertions(+), 58 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx index 0d2e937931e21..269f4db62f305 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx @@ -16,6 +16,7 @@ import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas import { createStubIndexPattern, stubIndexPattern } from '@kbn/data-plugin/common/stubs'; import { AddExceptionFlyout } from '.'; +import { initialState as exceptionItemsInitialState, createExceptionItemsReducer } from './reducer'; import { useFetchIndex } from '../../../../common/containers/source'; import { useCreateOrUpdateException } from '../../logic/use_create_update_exception'; import { useFetchIndexPatterns } from '../../logic/use_exception_flyout_data'; @@ -153,65 +154,123 @@ describe('When the add exception modal is opened', () => { expect(wrapper.find('ExceptionsAddToRulesOrLists').exists()).toBeFalsy(); }); - it('should show a warning callout if wildcard is used', async () => { - mockUseFetchIndex.mockImplementation(() => [ - false, - { - indexPatterns: stubIndexPattern, - }, - ]); + describe('warning callouts', () => { + let mountWrapper: ReactWrapper; + beforeEach(() => { + mockUseFetchIndex.mockImplementation(() => [ + false, + { + indexPatterns: stubIndexPattern, + }, + ]); + + mountWrapper = mount( + + + + ); + }); - const mountWrapper = mount( - - { + const callProps = mockGetExceptionBuilderComponentLazy.mock.calls[0][0]; + await waitFor(() => + callProps.onChange({ + exceptionItems: [ { - ...getRulesSchemaMock(), - index: ['filebeat-*'], - exceptions_list: [ + ...getExceptionListItemSchemaMock(), + entries: [ { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', + field: 'event.category', + operator: 'included', + type: 'match', + value: 'wildcardvalue*?', }, ], - } as Rule, - ]} - isBulkAction={false} - alertData={undefined} - isAlertDataLoading={undefined} - alertStatus={undefined} - isEndpointItem - showAlertCloseOptions - onCancel={jest.fn()} - onConfirm={jest.fn()} - /> - - ); - const callProps = mockGetExceptionBuilderComponentLazy.mock.calls[0][0]; - await waitFor(() => - callProps.onChange({ - exceptionItems: [ - { - ...getExceptionListItemSchemaMock(), - entries: [ - { - field: 'event.category', - operator: 'included', - type: 'match', - value: 'wildcardvalue*?', - }, - ], - }, - ], - }) - ); + }, + ], + }) + ); - mountWrapper.update(); - expect( - mountWrapper.find('[data-test-subj="wildcardWithWrongOperatorCallout"]').exists() - ).toBeTruthy(); + mountWrapper.update(); + expect( + mountWrapper.find('[data-test-subj="wildcardWithWrongOperatorCallout"]').exists() + ).toBeTruthy(); + }); + + it('should show a warning callout if there is a partial code signature entry with only subject_name', async () => { + const callProps = mockGetExceptionBuilderComponentLazy.mock.calls[0][0]; + await waitFor(() => + callProps.onChange({ + exceptionItems: [ + { + ...getExceptionListItemSchemaMock(), + entries: [ + { + field: 'process.code_signature.subject_name', + operator: 'included', + type: 'match', + value: 'asdf', + }, + ], + }, + ], + }) + ); + + mountWrapper.update(); + expect( + mountWrapper.find('[data-test-subj="partialCodeSignatureCallout"]').exists() + ).toBeTruthy(); + }); + + it('should show a warning callout if there is a partial code signature entry with only trusted field', async () => { + const callProps = mockGetExceptionBuilderComponentLazy.mock.calls[0][0]; + await waitFor(() => + callProps.onChange({ + exceptionItems: [ + { + ...getExceptionListItemSchemaMock(), + entries: [ + { + field: 'process.code_signature.trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + ], + }, + ], + }) + ); + + mountWrapper.update(); + expect( + mountWrapper.find('[data-test-subj="partialCodeSignatureCallout"]').exists() + ).toBeTruthy(); + }); }); }); @@ -961,4 +1020,15 @@ describe('When the add exception modal is opened', () => { }); }); }); + describe('the reducer', () => { + it('should update partialCodeSignatureWarningExists, when warning is true', () => { + const updatedState = createExceptionItemsReducer(); + expect( + updatedState(exceptionItemsInitialState, { + type: 'setPartialCodeSignature', + warningExists: true, + }) + ).toEqual({ ...exceptionItemsInitialState, partialCodeSignatureWarningExists: true }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx index 85790713cfa3f..092e3e779f5b5 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx @@ -26,13 +26,19 @@ import { import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import type { OsTypeArray, ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { hasWrongOperatorWithWildcard } from '@kbn/securitysolution-list-utils'; +import { + hasWrongOperatorWithWildcard, + hasPartialCodeSignatureEntry, +} from '@kbn/securitysolution-list-utils'; import type { ExceptionsBuilderExceptionItem, ExceptionsBuilderReturnExceptionItem, } from '@kbn/securitysolution-list-utils'; -import { WildCardWithWrongOperatorCallout } from '@kbn/securitysolution-exception-list-components'; +import { + WildCardWithWrongOperatorCallout, + PartialCodeSignatureCallout, +} from '@kbn/securitysolution-exception-list-components'; import type { Moment } from 'moment'; import type { Status } from '../../../../../common/api/detection_engine'; import * as i18n from './translations'; @@ -158,6 +164,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({ expireTime, expireErrorExists, wildcardWarningExists, + partialCodeSignatureWarningExists, }, dispatch, ] = useReducer(createExceptionItemsReducer(), { @@ -190,6 +197,10 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({ type: 'setWildcardWithWrongOperator', warningExists: hasWrongOperatorWithWildcard(items), }); + dispatch({ + type: 'setPartialCodeSignature', + warningExists: hasPartialCodeSignatureEntry(items), + }); dispatch({ type: 'setExceptionItems', items, @@ -564,6 +575,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({ getExtendedFields={getExtendedFields} /> {wildcardWarningExists && } + {partialCodeSignatureWarningExists && } {listType !== ExceptionListTypeEnum.ENDPOINT && !sharedListToAddTo?.length && ( <> diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/reducer.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/reducer.ts index 01b0ce85b45d1..e91c1c9328a7b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/reducer.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/reducer.ts @@ -35,6 +35,7 @@ export interface State { expireTime: Moment | undefined; expireErrorExists: boolean; wildcardWarningExists: boolean; + partialCodeSignatureWarningExists: boolean; } export const initialState: State = { @@ -57,6 +58,7 @@ export const initialState: State = { expireTime: undefined, expireErrorExists: false, wildcardWarningExists: false, + partialCodeSignatureWarningExists: false, }; export type Action = @@ -135,11 +137,15 @@ export type Action = | { type: 'setWildcardWithWrongOperator'; warningExists: boolean; + } + | { + type: 'setPartialCodeSignature'; + warningExists: boolean; }; export const createExceptionItemsReducer = () => - /* eslint complexity: ["error", 22]*/ + /* eslint complexity: ["error", 23]*/ (state: State, action: Action): State => { switch (action.type) { case 'setExceptionItemMeta': { @@ -184,6 +190,13 @@ export const createExceptionItemsReducer = wildcardWarningExists: warningExists, }; } + case 'setPartialCodeSignature': { + const { warningExists } = action; + return { + ...state, + partialCodeSignatureWarningExists: warningExists, + }; + } case 'setComment': { const { comment } = action; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.test.tsx index 07723db415daf..13c8c59e07732 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.test.tsx @@ -313,6 +313,58 @@ describe('When the edit exception modal is opened', () => { wrapper.find('[data-test-subj="wildcardWithWrongOperatorCallout"]').exists() ).toBeTruthy(); }); + + it('should show a warning callout if there is a partial code signature entry with only subject_name', async () => { + const callProps = mockGetExceptionBuilderComponentLazy.mock.calls[0][0]; + await waitFor(() => + callProps.onChange({ + exceptionItems: [ + { + ...getExceptionListItemSchemaMock(), + entries: [ + { + field: 'process.code_signature.subject_name', + operator: 'included', + type: 'match', + value: 'asdf', + }, + ], + }, + ], + }) + ); + + wrapper.update(); + expect( + wrapper.find('[data-test-subj="partialCodeSignatureCallout"]').exists() + ).toBeTruthy(); + }); + + it('should show a warning callout if there is a partial code signature entry with only trusted field', async () => { + const callProps = mockGetExceptionBuilderComponentLazy.mock.calls[0][0]; + await waitFor(() => + callProps.onChange({ + exceptionItems: [ + { + ...getExceptionListItemSchemaMock(), + entries: [ + { + field: 'process.code_signature.trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + ], + }, + ], + }) + ); + + wrapper.update(); + expect( + wrapper.find('[data-test-subj="partialCodeSignatureCallout"]').exists() + ).toBeTruthy(); + }); }); describe('when exception entry fields and index allow user to bulk close', () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx index b6336e3326e25..e45cb4ab742ad 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx @@ -26,10 +26,16 @@ import { ExceptionListTypeEnum, } from '@kbn/securitysolution-io-ts-list-types'; -import { hasWrongOperatorWithWildcard } from '@kbn/securitysolution-list-utils'; +import { + hasWrongOperatorWithWildcard, + hasPartialCodeSignatureEntry, +} from '@kbn/securitysolution-list-utils'; import type { ExceptionsBuilderReturnExceptionItem } from '@kbn/securitysolution-list-utils'; -import { WildCardWithWrongOperatorCallout } from '@kbn/securitysolution-exception-list-components'; +import { + WildCardWithWrongOperatorCallout, + PartialCodeSignatureCallout, +} from '@kbn/securitysolution-exception-list-components'; import type { Moment } from 'moment'; import moment from 'moment'; @@ -116,6 +122,7 @@ const EditExceptionFlyoutComponent: React.FC = ({ expireTime, expireErrorExists, wildcardWarningExists, + partialCodeSignatureWarningExists, }, dispatch, ] = useReducer(createExceptionItemsReducer(), { @@ -130,6 +137,7 @@ const EditExceptionFlyoutComponent: React.FC = ({ expireTime: itemToEdit.expire_time !== undefined ? moment(itemToEdit.expire_time) : undefined, expireErrorExists: false, wildcardWarningExists: false, + partialCodeSignatureWarningExists: false, }); const allowLargeValueLists = useMemo((): boolean => { @@ -170,6 +178,10 @@ const EditExceptionFlyoutComponent: React.FC = ({ type: 'setWildcardWithWrongOperator', warningExists: hasWrongOperatorWithWildcard(items), }); + dispatch({ + type: 'setPartialCodeSignature', + warningExists: hasPartialCodeSignatureEntry(items), + }); dispatch({ type: 'setExceptionItems', items, @@ -417,6 +429,7 @@ const EditExceptionFlyoutComponent: React.FC = ({ getExtendedFields={getExtendedFields} /> {wildcardWarningExists && } + {partialCodeSignatureWarningExists && } {!openedFromListDetailPage && listType === ExceptionListTypeEnum.DETECTION && ( <> diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/reducer.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/reducer.ts index 351af20900291..ee92dd28ca597 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/reducer.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/reducer.ts @@ -20,6 +20,7 @@ export interface State { expireTime: Moment | undefined; expireErrorExists: boolean; wildcardWarningExists: boolean; + partialCodeSignatureWarningExists: boolean; } export type Action = @@ -66,6 +67,10 @@ export type Action = | { type: 'setWildcardWithWrongOperator'; warningExists: boolean; + } + | { + type: 'setPartialCodeSignature'; + warningExists: boolean; }; export const createExceptionItemsReducer = @@ -162,6 +167,13 @@ export const createExceptionItemsReducer = wildcardWarningExists: warningExists, }; } + case 'setPartialCodeSignature': { + const { warningExists } = action; + return { + ...state, + partialCodeSignatureWarningExists: warningExists, + }; + } default: return state; }