From 3cb4e1ee99936582016568df0c2cd077a06e9c81 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 19 Nov 2024 02:08:56 +1100 Subject: [PATCH] [8.x] [EDR Workflows] Add Signer option to Mac trusted apps (#197821) (#200566) # Backport This will backport the following commits from `main` to `8.x`: - [[EDR Workflows] Add Signer option to Mac trusted apps (#197821)](https://github.com/elastic/kibana/pull/197821) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Konrad Szwarc --- .../src/path_validations/index.ts | 7 +- .../exceptions_list_item_generator.ts | 39 ++- .../endpoint/schema/trusted_apps.test.ts | 36 +-- .../common/endpoint/schema/trusted_apps.ts | 9 + .../common/endpoint/types/trusted_apps.ts | 21 +- .../cypress/e2e/artifacts/trusted_apps.cy.ts | 231 ++++++++++++++++++ .../management/cypress/tasks/artifacts.ts | 124 +++++++++- .../pages/trusted_apps/state/type_guards.ts | 18 +- .../condition_entry_input/index.test.tsx | 4 +- .../condition_entry_input/index.tsx | 14 +- .../trusted_apps/view/components/form.tsx | 57 +++-- .../pages/trusted_apps/view/translations.ts | 16 +- .../validators/trusted_app_validator.ts | 42 +++- .../trusted_apps.ts | 73 ++++-- 14 files changed, 586 insertions(+), 105 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/trusted_apps.cy.ts diff --git a/packages/kbn-securitysolution-utils/src/path_validations/index.ts b/packages/kbn-securitysolution-utils/src/path_validations/index.ts index 0609129349b60..1f1eaf0b01423 100644 --- a/packages/kbn-securitysolution-utils/src/path_validations/index.ts +++ b/packages/kbn-securitysolution-utils/src/path_validations/index.ts @@ -21,19 +21,22 @@ export enum ConditionEntryField { HASH = 'process.hash.*', PATH = 'process.executable.caseless', SIGNER = 'process.Ext.code_signature', + SIGNER_MAC = 'process.code_signature', } export enum EntryFieldType { HASH = '.hash.', EXECUTABLE = '.executable.caseless', PATH = '.path', - SIGNER = '.Ext.code_signature', + SIGNER = '.code_signature', } export type TrustedAppConditionEntryField = | 'process.hash.*' | 'process.executable.caseless' - | 'process.Ext.code_signature'; + | 'process.Ext.code_signature' + | 'process.code_signature'; + export type BlocklistConditionEntryField = | 'file.hash.*' | 'file.path' diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts index cb332f8dea55a..b7293a3cef16c 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts @@ -9,13 +9,9 @@ import type { ExceptionListItemSchema, CreateExceptionListItemSchema, UpdateExceptionListItemSchema, + EntriesArray, } from '@kbn/securitysolution-io-ts-list-types'; -import { - ENDPOINT_EVENT_FILTERS_LIST_ID, - ENDPOINT_TRUSTED_APPS_LIST_ID, - ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, - ENDPOINT_BLOCKLISTS_LIST_ID, -} from '@kbn/securitysolution-list-constants'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; import { ConditionEntryField } from '@kbn/securitysolution-utils'; import { BaseDataGenerator } from './base_data_generator'; import { BY_POLICY_ARTIFACT_TAG_PREFIX, GLOBAL_ARTIFACT_TAG } from '../service/artifacts/constants'; @@ -150,7 +146,7 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator = {}): ExceptionListItemSchema { return this.generate({ name: `Trusted app (${this.randomString(5)})`, - list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + list_id: ENDPOINT_ARTIFACT_LISTS.trustedApps.id, ...overrides, }); } @@ -173,10 +169,33 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator = {}): ExceptionListItemSchema { return this.generate({ name: `Event filter (${this.randomString(5)})`, - list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + list_id: ENDPOINT_ARTIFACT_LISTS.eventFilters.id, entries: [ { field: 'process.pe.company', @@ -224,7 +243,7 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator { @@ -105,14 +105,15 @@ describe('When invoking Trusted Apps Schema', () => { value: 'c:/programs files/Anti-Virus', ...(data || {}), }); - const createNewTrustedApp = (data?: T): NewTrustedApp => ({ - name: 'Some Anti-Virus App', - description: 'this one is ok', - os: OperatingSystem.WINDOWS, - effectScope: { type: 'global' }, - entries: [createConditionEntry()], - ...(data || {}), - }); + const createNewTrustedApp = (data?: T): NewTrustedApp => + ({ + name: 'Some Anti-Virus App', + description: 'this one is ok', + os: OperatingSystem.WINDOWS, + effectScope: { type: 'global' }, + entries: [createConditionEntry()], + ...(data || {}), + } as NewTrustedApp); const body = PostTrustedAppCreateRequestSchema.body; it('should not error on a valid message', () => { @@ -389,14 +390,15 @@ describe('When invoking Trusted Apps Schema', () => { value: 'c:/programs files/Anti-Virus', ...(data || {}), }); - const createNewTrustedApp = (data?: T): NewTrustedApp => ({ - name: 'Some Anti-Virus App', - description: 'this one is ok', - os: OperatingSystem.WINDOWS, - effectScope: { type: 'global' }, - entries: [createConditionEntry()], - ...(data || {}), - }); + const createNewTrustedApp = (data?: T): NewTrustedApp => + ({ + name: 'Some Anti-Virus App', + description: 'this one is ok', + os: OperatingSystem.WINDOWS, + effectScope: { type: 'global' }, + entries: [createConditionEntry()], + ...(data || {}), + } as NewTrustedApp); const updateParams = (data?: T): PutTrustedAppsRequestParams => ({ id: 'validId', diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index 4b0a1ee2157de..e31e65195ec95 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -80,6 +80,15 @@ const LinuxEntrySchema = schema.object({ const MacEntrySchema = schema.object({ ...CommonEntrySchema, + field: schema.oneOf([ + schema.literal(ConditionEntryField.HASH), + schema.literal(ConditionEntryField.PATH), + schema.literal(ConditionEntryField.SIGNER_MAC), + ]), + value: schema.string({ + validate: (field: string) => + field.length > 0 ? undefined : `invalidField.${ConditionEntryField.SIGNER_MAC}`, + }), }); const entriesSchemaOptions = { diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index dd97ad02dcb96..4fa99c015a7c7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -36,23 +36,32 @@ export interface TrustedAppConditionEntry; export type WindowsConditionEntry = TrustedAppConditionEntry< ConditionEntryField.HASH | ConditionEntryField.PATH | ConditionEntryField.SIGNER >; -export interface MacosLinuxConditionEntries { - os: OperatingSystem.LINUX | OperatingSystem.MAC; - entries: MacosLinuxConditionEntry[]; +export type MacosConditionEntry = TrustedAppConditionEntry< + ConditionEntryField.HASH | ConditionEntryField.PATH | ConditionEntryField.SIGNER_MAC +>; + +interface LinuxConditionEntries { + os: OperatingSystem.LINUX; + entries: LinuxConditionEntry[]; } -export interface WindowsConditionEntries { +interface WindowsConditionEntries { os: OperatingSystem.WINDOWS; entries: WindowsConditionEntry[]; } +interface MacosConditionEntries { + os: OperatingSystem.MAC; + entries: MacosConditionEntry[]; +} + export interface GlobalEffectScope { type: 'global'; } @@ -70,7 +79,7 @@ export type NewTrustedApp = { name: string; description?: string; effectScope: EffectScope; -} & (MacosLinuxConditionEntries | WindowsConditionEntries); +} & (LinuxConditionEntries | WindowsConditionEntries | MacosConditionEntries); /** A trusted app entry */ export type TrustedApp = NewTrustedApp & { diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/trusted_apps.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/trusted_apps.cy.ts new file mode 100644 index 0000000000000..aef24ed4ce045 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/trusted_apps.cy.ts @@ -0,0 +1,231 @@ +/* + * 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 { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; +import { + createArtifactList, + createPerPolicyArtifact, + removeExceptionsList, + trustedAppsFormSelectors, +} from '../../tasks/artifacts'; +import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; +import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../tasks/fleet'; +import { login } from '../../tasks/login'; + +const { + openTrustedApps, + selectOs, + openFieldSelector, + expectedFieldOptions, + selectField, + fillOutValueField, + fillOutTrustedAppsFlyout, + submitForm, + validateSuccessPopup, + validateRenderedCondition, + clickAndConditionButton, + validateRenderedConditions, + deleteTrustedAppItem, + removeSingleCondition, + expectAllFieldOptionsRendered, + expectFieldOptionsNotRendered, +} = trustedAppsFormSelectors; + +describe( + 'Trusted Apps', + { + tags: ['@ess', '@serverless', '@skipInServerlessMKI'], // @skipInServerlessMKI until kibana is rebuilt after merge + }, + () => { + let indexedPolicy: IndexedFleetEndpointPolicyResponse; + + before(() => { + getEndpointIntegrationVersion().then((version) => { + createAgentPolicyTask(version).then((data) => { + indexedPolicy = data; + }); + }); + }); + + beforeEach(() => { + login(); + }); + + after(() => { + if (indexedPolicy) { + cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy); + } + }); + + const createArtifactBodyRequest = (multiCondition = false) => ({ + list_id: ENDPOINT_ARTIFACT_LISTS.trustedApps.id, + entries: [ + { + entries: [ + { + field: 'trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + { + field: 'subject_name', + value: 'TestSignature', + type: 'match', + operator: 'included', + }, + ], + field: 'process.code_signature', + type: 'nested', + }, + ...(multiCondition + ? [ + { + field: 'process.hash.sha1', + value: '323769d194406183912bb903e7fe738221543348', + type: 'match', + operator: 'included', + }, + { + field: 'process.executable.caseless', + value: '/dev/null', + type: 'match', + operator: 'included', + }, + ] + : []), + ], + os_types: ['macos'], + }); + + describe('Renders Trusted Apps form fields', () => { + it('Correctly renders all blocklist fields for different OSs', () => { + openTrustedApps({ create: true }); + selectOs('windows'); + expectFieldOptionsNotRendered(); + openFieldSelector(); + expectAllFieldOptionsRendered(); + + selectOs('macos'); + expectFieldOptionsNotRendered(); + openFieldSelector(); + expectAllFieldOptionsRendered(); + + selectOs('linux'); + expectFieldOptionsNotRendered(); + openFieldSelector(); + expectedFieldOptions(['Path', 'Hash']); + }); + }); + + describe('Handles CRUD with signature field', () => { + afterEach(() => { + removeExceptionsList(ENDPOINT_ARTIFACT_LISTS.trustedApps.id); + }); + + it('Correctly creates a trusted app with a single signature field on Mac', () => { + const expectedCondition = /AND\s*process\.code_signature\s*IS\s*TestSignature/; + + openTrustedApps({ create: true }); + fillOutTrustedAppsFlyout(); + selectOs('macos'); + openFieldSelector(); + selectField(); + fillOutValueField('TestSignature'); + submitForm(); + validateSuccessPopup('create'); + validateRenderedCondition(expectedCondition); + }); + + describe('Correctly updates and deletes Mac os trusted app with single signature field', () => { + let itemId: string; + + beforeEach(() => { + createArtifactList(ENDPOINT_ARTIFACT_LISTS.trustedApps.id); + createPerPolicyArtifact('Test TrustedApp', createArtifactBodyRequest()).then( + (response) => { + itemId = response.body.item_id; + } + ); + }); + + it('Updates Mac os single signature field trusted app item', () => { + const expectedCondition = /AND\s*process\.code_signature\s*IS\s*TestSignatureNext/; + openTrustedApps({ itemId }); + fillOutValueField('Next'); + submitForm(); + validateSuccessPopup('update'); + validateRenderedCondition(expectedCondition); + }); + + it('Deletes a blocklist item', () => { + openTrustedApps(); + deleteTrustedAppItem(); + validateSuccessPopup('delete'); + }); + }); + + it('Correctly creates a trusted app with a multiple conditions on Mac', () => { + const expectedCondition = + /\s*OSIS\s*Mac\s*AND\s*process\.code_signature\s*IS\s*TestSignature\s*AND\s*process\.hash\.\*\s*IS\s*323769d194406183912bb903e7fe738221543348\s*AND\s*process\.executable\.caselessIS\s*\/dev\/null\s*/; + + openTrustedApps({ create: true }); + fillOutTrustedAppsFlyout(); + selectOs('macos'); + // Set signature field + openFieldSelector(); + selectField(); + fillOutValueField('TestSignature'); + // Add another condition + clickAndConditionButton(); + // Set hash field + openFieldSelector(1, 1); + selectField('Hash', 1, 1); + fillOutValueField('323769d194406183912bb903e7fe738221543348', 1, 1); + // Add another condition + clickAndConditionButton(); + // Set path field + openFieldSelector(1, 2); + selectField('Path', 1, 2); + fillOutValueField('/dev/null', 1, 2); + + submitForm(); + validateSuccessPopup('create'); + validateRenderedConditions(expectedCondition); + }); + + describe('Correctly updates and deletes Mac os trusted app with multiple conditions', () => { + let itemId: string; + + beforeEach(() => { + createArtifactList(ENDPOINT_ARTIFACT_LISTS.trustedApps.id); + createPerPolicyArtifact('Test TrustedApp', createArtifactBodyRequest(true)).then( + (response) => { + itemId = response.body.item_id; + } + ); + }); + + it('Updates Mac os multiple condition trusted app item', () => { + const expectedCondition = + /\s*AND\s*process\.code_signature\s*IS\s*TestSignature\s*AND\s*process\.executable\.caselessIS\s*\/dev\/null\s*/; + openTrustedApps({ itemId }); + removeSingleCondition(1, 1); + submitForm(); + validateSuccessPopup('update'); + validateRenderedCondition(expectedCondition); + }); + + it('Deletes a blocklist item', () => { + openTrustedApps(); + deleteTrustedAppItem(); + validateSuccessPopup('delete'); + }); + }); + }); + } +); diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts index fdffa0bd03381..034ea11d87a83 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts @@ -18,7 +18,7 @@ import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL, } from '@kbn/securitysolution-list-constants'; -import { APP_BLOCKLIST_PATH } from '../../../../common/constants'; +import { APP_BLOCKLIST_PATH, APP_TRUSTED_APPS_PATH } from '../../../../common/constants'; import { loadPage, request } from './common'; export const removeAllArtifacts = () => { @@ -108,6 +108,128 @@ export const yieldFirstPolicyID = (): Cypress.Chainable => return body.items[0].id; }); +export const trustedAppsFormSelectors = { + selectOs: (os: 'windows' | 'macos' | 'linux') => { + cy.getByTestSubj('trustedApps-form-osSelectField').click(); + cy.get(`button[role="option"][id="${os}"]`).click(); + }, + + openFieldSelector: (group = 1, entry = 0) => { + cy.getByTestSubj( + `trustedApps-form-conditionsBuilder-group${group}-entry${entry}-field` + ).click(); + }, + + selectField: (field: 'Signature' | 'Hash' | 'Path' = 'Signature', group = 1, entry = 0) => { + cy.getByTestSubj( + `trustedApps-form-conditionsBuilder-group${group}-entry${entry}-field-type-${field}` + ).click(); + }, + + fillOutValueField: (value: string, group = 1, entry = 0) => { + cy.getByTestSubj(`trustedApps-form-conditionsBuilder-group${group}-entry${entry}-value`).type( + value + ); + }, + + clickAndConditionButton: () => { + cy.getByTestSubj('trustedApps-form-conditionsBuilder-group1-AndButton').click(); + }, + + submitForm: () => { + cy.getByTestSubj('trustedAppsListPage-flyout-submitButton').click(); + }, + + fillOutTrustedAppsFlyout: () => { + cy.getByTestSubj('trustedApps-form-nameTextField').type('Test TrustedApp'); + cy.getByTestSubj('trustedApps-form-descriptionField').type('Test Description'); + }, + + expectedFieldOptions: (fields = ['Path', 'Hash', 'Signature']) => { + if (fields.length) { + fields.forEach((field) => { + cy.getByTestSubj( + `trustedApps-form-conditionsBuilder-group1-entry0-field-type-${field}` + ).contains(field); + }); + } else { + const fields2 = ['Path', 'Hash', 'Signature']; + fields2.forEach((field) => { + cy.getByTestSubj( + `trustedApps-form-conditionsBuilder-group1-entry0-field-type-${field}` + ).should('not.exist'); + }); + } + }, + + expectAllFieldOptionsRendered: () => { + trustedAppsFormSelectors.expectedFieldOptions(); + }, + + expectFieldOptionsNotRendered: () => { + trustedAppsFormSelectors.expectedFieldOptions([]); + }, + + openTrustedApps: ({ create, itemId }: { create?: boolean; itemId?: string } = {}) => { + if (!create && !itemId) { + loadPage(APP_TRUSTED_APPS_PATH); + } else if (create) { + loadPage(`${APP_TRUSTED_APPS_PATH}?show=create`); + } else if (itemId) { + loadPage(`${APP_TRUSTED_APPS_PATH}?itemId=${itemId}&show=edit`); + } + }, + + validateSuccessPopup: (type: 'create' | 'update' | 'delete') => { + let expectedTitle = ''; + switch (type) { + case 'create': + expectedTitle = '"Test TrustedApp" has been added to your trusted applications.'; + break; + case 'update': + expectedTitle = '"Test TrustedApp" has been updated'; + break; + case 'delete': + expectedTitle = '"Test TrustedApp" has been removed from trusted applications.'; + break; + } + cy.getByTestSubj('euiToastHeader__title').contains(expectedTitle); + }, + + validateRenderedCondition: (expectedCondition: RegExp) => { + cy.getByTestSubj('trustedAppsListPage-card') + .first() + .within(() => { + cy.getByTestSubj('trustedAppsListPage-card-criteriaConditions-os') + .invoke('text') + .should('match', /OS\s*IS\s*Mac/); + cy.getByTestSubj('trustedAppsListPage-card-criteriaConditions-condition') + .invoke('text') + .should('match', expectedCondition); + }); + }, + validateRenderedConditions: (expectedConditions: RegExp) => { + cy.getByTestSubj('trustedAppsListPage-card-criteriaConditions') + .invoke('text') + .should('match', expectedConditions); + }, + removeSingleCondition: (group = 1, entry = 0) => { + cy.getByTestSubj( + `trustedApps-form-conditionsBuilder-group${group}-entry${entry}-remove` + ).click(); + }, + deleteTrustedAppItem: () => { + cy.getByTestSubj('trustedAppsListPage-card') + .first() + .within(() => { + cy.getByTestSubj('trustedAppsListPage-card-header-actions-button').click(); + }); + + cy.getByTestSubj('trustedAppsListPage-card-cardDeleteAction').click(); + cy.getByTestSubj('trustedAppsListPage-deleteModal-submitButton').click(); + }, +}; + export const blocklistFormSelectors = { expectSingleOperator: (field: 'Path' | 'Signature' | 'Hash') => { cy.getByTestSubj('blocklist-form-field-select').contains(field); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts index 082435817d43d..f6dff90b48227 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts @@ -8,18 +8,14 @@ import { ConditionEntryField } from '@kbn/securitysolution-utils'; import type { TrustedAppConditionEntry, - MacosLinuxConditionEntry, - WindowsConditionEntry, + LinuxConditionEntry, } from '../../../../../common/endpoint/types'; -export const isWindowsTrustedAppCondition = ( +export const isSignerFieldExcluded = ( condition: TrustedAppConditionEntry -): condition is WindowsConditionEntry => { - return condition.field === ConditionEntryField.SIGNER || true; -}; - -export const isMacosLinuxTrustedAppCondition = ( - condition: TrustedAppConditionEntry -): condition is MacosLinuxConditionEntry => { - return condition.field !== ConditionEntryField.SIGNER; +): condition is LinuxConditionEntry => { + return ( + condition.field !== ConditionEntryField.SIGNER && + condition.field !== ConditionEntryField.SIGNER_MAC + ); }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx index 6ba6a0b91d4eb..100e10e16cb99 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx @@ -152,13 +152,13 @@ describe('Condition entry input', () => { expect(superSelectProps.options.length).toBe(2); }); - it('should be able to select two options when MAC OS', () => { + it('should be able to select three options when MAC OS', () => { const element = mount(getElement('testCheckSignatureOption', { os: OperatingSystem.MAC })); const superSelectProps = element .find('[data-test-subj="testCheckSignatureOption-field"]') .first() .props() as EuiSuperSelectProps; - expect(superSelectProps.options.length).toBe(2); + expect(superSelectProps.options.length).toBe(3); }); it('should have operator value selected when field is HASH', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx index 3ce26c70d3186..b55c86b939395 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx @@ -143,6 +143,18 @@ export const ConditionEntryInput = memo( }, ] : []), + ...(os === OperatingSystem.MAC + ? [ + { + dropdownDisplay: getDropdownDisplay(ConditionEntryField.SIGNER_MAC), + inputDisplay: CONDITION_FIELD_TITLE[ConditionEntryField.SIGNER_MAC], + value: ConditionEntryField.SIGNER_MAC, + 'data-test-subj': getTestId( + `field-type-${CONDITION_FIELD_TITLE[ConditionEntryField.SIGNER_MAC]}` + ), + }, + ] + : []), ]; }, [getTestId, os]); @@ -224,7 +236,7 @@ export const ConditionEntryInput = memo( - {/* Unicode `nbsp` is used below so that Remove button is property displayed */} + {/* Unicode `nbsp` is used below so that Remove button is properly displayed */} ( entries: [] as ArtifactFormComponentProps['item']['entries'], }; - if (os !== OperatingSystem.WINDOWS) { - const macOsLinuxConditionEntry = item.entries.filter((entry) => - isMacosLinuxTrustedAppCondition(entry as TrustedAppConditionEntry) - ); - nextItem.entries.push(...macOsLinuxConditionEntry); - if (item.entries.length === 0) { - nextItem.entries.push(defaultConditionEntry()); - } - } else { - nextItem.entries.push(...item.entries); + switch (os) { + case OperatingSystem.LINUX: + nextItem.entries = item.entries.filter((entry) => + isSignerFieldExcluded(entry as TrustedAppConditionEntry) + ); + if (item.entries.length === 0) { + nextItem.entries.push(defaultConditionEntry()); + } + break; + case OperatingSystem.MAC: + nextItem.entries = item.entries.map((entry) => + entry.field === ConditionEntryField.SIGNER + ? { ...entry, field: ConditionEntryField.SIGNER_MAC } + : entry + ); + if (item.entries.length === 0) { + nextItem.entries.push(defaultConditionEntry()); + } + break; + case OperatingSystem.WINDOWS: + nextItem.entries = item.entries.map((entry) => + entry.field === ConditionEntryField.SIGNER_MAC + ? { ...entry, field: ConditionEntryField.SIGNER } + : entry + ); + if (item.entries.length === 0) { + nextItem.entries.push(defaultConditionEntry()); + } + break; + default: + nextItem.entries.push(...item.entries); + break; } processChanged(nextItem); @@ -429,17 +448,15 @@ export const TrustedAppsForm = memo( entries: [], }; const os = ((item.os_types ?? [])[0] as OperatingSystem) ?? OperatingSystem.WINDOWS; - if (os === OperatingSystem.WINDOWS) { - nextItem.entries = [...item.entries, defaultConditionEntry()].filter((entry) => - isWindowsTrustedAppCondition(entry as TrustedAppConditionEntry) - ); - } else { + if (os === OperatingSystem.LINUX) { nextItem.entries = [ ...item.entries.filter((entry) => - isMacosLinuxTrustedAppCondition(entry as TrustedAppConditionEntry) + isSignerFieldExcluded(entry as TrustedAppConditionEntry) ), defaultConditionEntry(), ]; + } else { + nextItem.entries = [...item.entries, defaultConditionEntry()]; } processChanged(nextItem); setHasFormChanged(true); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts index c1e544be537ef..0208898617c49 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -8,9 +8,10 @@ import { i18n } from '@kbn/i18n'; import { ConditionEntryField } from '@kbn/securitysolution-utils'; import type { - MacosLinuxConditionEntry, + LinuxConditionEntry, WindowsConditionEntry, OperatorFieldIds, + MacosConditionEntry, } from '../../../../../common/endpoint/types'; export const NAME_LABEL = i18n.translate('xpack.securitySolution.trustedApps.name.label', { @@ -68,6 +69,10 @@ export const CONDITION_FIELD_TITLE: { [K in ConditionEntryField]: string } = { 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.signature', { defaultMessage: 'Signature' } ), + [ConditionEntryField.SIGNER_MAC]: i18n.translate( + 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.signatureMac', + { defaultMessage: 'Signature' } + ), }; export const CONDITION_FIELD_DESCRIPTION: { [K in ConditionEntryField]: string } = { @@ -83,6 +88,10 @@ export const CONDITION_FIELD_DESCRIPTION: { [K in ConditionEntryField]: string } 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.description.signature', { defaultMessage: 'The signer of the application' } ), + [ConditionEntryField.SIGNER_MAC]: i18n.translate( + 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.description.signatureMac', + { defaultMessage: 'The signer of the application' } + ), }; export const OPERATOR_TITLES: { [K in OperatorFieldIds]: string } = { @@ -95,7 +104,10 @@ export const OPERATOR_TITLES: { [K in OperatorFieldIds]: string } = { }; export const ENTRY_PROPERTY_TITLES: Readonly<{ - [K in keyof Omit]: string; + [K in keyof Omit< + LinuxConditionEntry | WindowsConditionEntry | MacosConditionEntry, + 'type' + >]: string; }> = { field: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.entry.field', { defaultMessage: 'Field', diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts index 38dd3442f3b4f..3b8a77a964006 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; import type { TypeOf } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; @@ -30,7 +30,8 @@ const ProcessHashField = schema.oneOf([ schema.literal('process.hash.sha256'), ]); const ProcessExecutablePath = schema.literal('process.executable.caseless'); -const ProcessCodeSigner = schema.literal('process.Ext.code_signature'); +const ProcessWindowsCodeSigner = schema.literal('process.Ext.code_signature'); +const ProcessMacCodeSigner = schema.literal('process.code_signature'); const ConditionEntryTypeSchema = schema.conditional( schema.siblingRef('field'), @@ -43,7 +44,8 @@ const ConditionEntryOperatorSchema = schema.literal('included'); type ConditionEntryFieldAllowedType = | TypeOf | TypeOf - | TypeOf; + | TypeOf + | TypeOf; type TrustedAppConditionEntry< T extends ConditionEntryFieldAllowedType = ConditionEntryFieldAllowedType @@ -54,7 +56,8 @@ type TrustedAppConditionEntry< operator: 'included'; value: string; } - | TypeOf; + | TypeOf + | TypeOf; /* * A generic Entry schema to be used for a specific entry schema depending on the OS @@ -85,11 +88,10 @@ const CommonEntrySchema = { ), }; -// Windows Signer entries use a Nested field that checks to ensure +// Windows/MacOS Signer entries use a Nested field that checks to ensure // that the certificate is trusted -const WindowsSignerEntrySchema = schema.object({ +const SignerEntrySchema = { type: schema.literal('nested'), - field: ProcessCodeSigner, entries: schema.arrayOf( schema.oneOf([ schema.object({ @@ -107,21 +109,35 @@ const WindowsSignerEntrySchema = schema.object({ ]), { minSize: 2, maxSize: 2 } ), +}; + +const SignerWindowsEntrySchema = schema.object({ + ...SignerEntrySchema, + field: ProcessWindowsCodeSigner, +}); + +const SignerMacEntrySchema = schema.object({ + ...SignerEntrySchema, + field: ProcessMacCodeSigner, }); const WindowsEntrySchema = schema.oneOf([ - WindowsSignerEntrySchema, + SignerWindowsEntrySchema, schema.object({ ...CommonEntrySchema, field: schema.oneOf([ProcessHashField, ProcessExecutablePath]), }), ]); -const LinuxEntrySchema = schema.object({ - ...CommonEntrySchema, -}); +const MacEntrySchema = schema.oneOf([ + SignerMacEntrySchema, + schema.object({ + ...CommonEntrySchema, + field: schema.oneOf([ProcessHashField, ProcessExecutablePath]), + }), +]); -const MacEntrySchema = schema.object({ +const LinuxEntrySchema = schema.object({ ...CommonEntrySchema, }); @@ -172,7 +188,7 @@ const TrustedAppDataSchema = schema.object( export class TrustedAppValidator extends BaseValidator { static isTrustedApp(item: { listId: string }): boolean { - return item.listId === ENDPOINT_TRUSTED_APPS_LIST_ID; + return item.listId === ENDPOINT_ARTIFACT_LISTS.trustedApps.id; } protected async validateHasWritePrivilege(): Promise { diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/trusted_apps.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/trusted_apps.ts index cb310acf47aa4..e7d693111aa05 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/trusted_apps.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/trusted_apps.ts @@ -206,26 +206,7 @@ export default function ({ getService }: FtrProviderContext) { const body = trustedAppApiCall.getBody(); body.os_types = ['linux']; - body.entries = [ - { - field: 'process.Ext.code_signature', - entries: [ - { - field: 'trusted', - value: 'true', - type: 'match', - operator: 'included', - }, - { - field: 'subject_name', - value: 'foo', - type: 'match', - operator: 'included', - }, - ], - type: 'nested', - }, - ]; + body.entries = exceptionsGenerator.generateTrustedAppSignerEntry(); await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path) .set('kbn-xsrf', 'true') @@ -235,6 +216,58 @@ export default function ({ getService }: FtrProviderContext) { .expect(anErrorMessageWith(/^.*(?!process\.Ext\.code_signature)/)); }); + it(`should error on [${trustedAppApiCall.method} if Mac signer field is used for Windows entry`, async () => { + const body = trustedAppApiCall.getBody(); + + body.os_types = ['windows']; + body.entries = exceptionsGenerator.generateTrustedAppSignerEntry('mac'); + + await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path) + .set('kbn-xsrf', 'true') + .send(body) + .expect(400); + }); + + it(`should error on [${trustedAppApiCall.method} if Windows signer field is used for Mac entry`, async () => { + const body = trustedAppApiCall.getBody(); + + body.os_types = ['macos']; + body.entries = exceptionsGenerator.generateTrustedAppSignerEntry(); + + await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path) + .set('kbn-xsrf', 'true') + .send(body) + .expect(400); + }); + + it('should not error if signer is set for a windows os entry item', async () => { + const body = trustedAppApiCalls[0].getBody(); + + body.os_types = ['windows']; + body.entries = exceptionsGenerator.generateTrustedAppSignerEntry(); + + await endpointPolicyManagerSupertest[trustedAppApiCalls[0].method]( + trustedAppApiCalls[0].path + ) + .set('kbn-xsrf', 'true') + .send(body) + .expect(200); + }); + + it('should not error if signer is set for a mac os entry item', async () => { + const body = trustedAppApiCalls[0].getBody(); + + body.os_types = ['macos']; + body.entries = exceptionsGenerator.generateTrustedAppSignerEntry('mac'); + + await endpointPolicyManagerSupertest[trustedAppApiCalls[0].method]( + trustedAppApiCalls[0].path + ) + .set('kbn-xsrf', 'true') + .send(body) + .expect(200); + }); + it(`should error on [${trustedAppApiCall.method}] if more than one OS is set`, async () => { const body = trustedAppApiCall.getBody();