diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/trusted_app_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/trusted_app_generator.ts index 6c691894103be..6384d8ffd509f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/trusted_app_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/trusted_app_generator.ts @@ -67,13 +67,29 @@ export class TrustedAppGenerator extends BaseDataGenerator { ...(scopeType === 'policy' ? { policies: this.randomArray(5, () => this.randomUUID()) } : {}), }) as EffectScope; + const os = this.randomOSFamily(); + const pathEntry = this.randomChoice([ + { + field: ConditionEntryField.PATH, + operator: 'included', + type: 'match', + value: os !== 'windows' ? '/one/two/three' : 'c:\\fol\\bin.exe', + }, + { + field: ConditionEntryField.PATH, + operator: 'included', + type: 'wildcard', + value: os !== 'windows' ? '/one/t*/*re/three.app' : 'c:\\fol*\\*ub*\\bin.exe', + }, + ]); + // TS types are conditional when it comes to the combination of OS and ENTRIES // @ts-expect-error TS2322 return merge( { description: `Generator says we trust ${name}`, name, - os: this.randomOSFamily(), + os, effectScope, entries: [ { @@ -82,12 +98,7 @@ export class TrustedAppGenerator extends BaseDataGenerator { type: 'match', value: '1234234659af249ddf3e40864e9fb241', }, - { - field: ConditionEntryField.PATH, - operator: 'included', - type: 'match', - value: '/one/two/three', - }, + pathEntry, ], }, overrides diff --git a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.test.ts b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.test.ts index ae95c21630bd8..952a2fa234ace 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isPathValid } from './validations'; +import { isPathValid, hasSimpleExecutableName } from './validations'; import { OperatingSystem, ConditionEntryField } from '../../types'; describe('Unacceptable Windows wildcard paths', () => { @@ -504,3 +504,58 @@ describe('Unacceptable Mac/Linux exact paths', () => { ).toEqual(false); }); }); + +describe('Executable filenames with wildcard PATHS', () => { + it('should return TRUE when MAC/LINUX wildcard paths have an executable name', () => { + expect( + hasSimpleExecutableName({ + os: OperatingSystem.LINUX, + type: 'wildcard', + value: '/opt/*/app', + }) + ).toEqual(true); + expect( + hasSimpleExecutableName({ + os: OperatingSystem.MAC, + type: 'wildcard', + value: '/op*/**/app.dmg', + }) + ).toEqual(true); + }); + + it('should return TRUE when WINDOWS wildcards paths have a executable name', () => { + expect( + hasSimpleExecutableName({ + os: OperatingSystem.WINDOWS, + type: 'wildcard', + value: 'c:\\**\\path.exe', + }) + ).toEqual(true); + }); + + it('should return FALSE when MAC/LINUX wildcard paths have a wildcard in executable name', () => { + expect( + hasSimpleExecutableName({ + os: OperatingSystem.LINUX, + type: 'wildcard', + value: '/op/*/*pp', + }) + ).toEqual(false); + expect( + hasSimpleExecutableName({ + os: OperatingSystem.MAC, + type: 'wildcard', + value: '/op*/b**/ap.m**', + }) + ).toEqual(false); + }); + it('should return FALSE when WINDOWS wildcards paths have a wildcard in executable name', () => { + expect( + hasSimpleExecutableName({ + os: OperatingSystem.WINDOWS, + type: 'wildcard', + value: 'c:\\**\\pa*h.exe', + }) + ).toEqual(false); + }); +}); diff --git a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts index 0fe3c0269bd15..7831ff78b91cb 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts @@ -34,6 +34,36 @@ export const getDuplicateFields = (entries: ConditionEntry[]) => { .map((entry) => entry[0]); }; +const WIN_EXEC_PATH = /\\(\w+\.\w+)$/i; +const UNIX_EXEC_PATH = /\/\w+\.*\w*$/i; + +export const getExecutableName = ({ + os, + value, +}: { + os: OperatingSystem; + value: string; +}): string => { + const execName = + os === OperatingSystem.WINDOWS ? value.match(WIN_EXEC_PATH) : value.match(UNIX_EXEC_PATH); + return execName ? execName[0].replaceAll(/\/|\\/gi, '') : ''; +}; + +export const hasSimpleExecutableName = ({ + os, + type, + value, +}: { + os: OperatingSystem; + type: TrustedAppEntryTypes; + value: string; +}): boolean => { + if (type === 'wildcard') { + return os === OperatingSystem.WINDOWS ? WIN_EXEC_PATH.test(value) : UNIX_EXEC_PATH.test(value); + } + return true; +}; + export const isPathValid = ({ os, field, diff --git a/x-pack/plugins/security_solution/common/utils/path_placeholder.ts b/x-pack/plugins/security_solution/common/utils/path_placeholder.ts index bba01b6d05b65..b7dd541c6cc03 100644 --- a/x-pack/plugins/security_solution/common/utils/path_placeholder.ts +++ b/x-pack/plugins/security_solution/common/utils/path_placeholder.ts @@ -9,11 +9,11 @@ import { ConditionEntryField, OperatingSystem, TrustedAppEntryTypes } from '../e export const getPlaceholderText = () => ({ windows: { - wildcard: 'C:\\sample\\**\\*', + wildcard: 'C:\\sample\\**\\path.exe', exact: 'C:\\sample\\path.exe', }, others: { - wildcard: '/opt/**/*', + wildcard: '/opt/**/app', exact: '/opt/bin', }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx index da925ddd8a6c1..b14e4bcb0b4f5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx @@ -30,6 +30,7 @@ import { import { isValidHash, isPathValid, + hasSimpleExecutableName, } from '../../../../../../common/endpoint/service/trusted_apps/validations'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; @@ -136,6 +137,13 @@ const validateFormValues = (values: MaybeImmutable): ValidationRe ); } else { values.entries.forEach((entry, index) => { + const isValidPathEntry = isPathValid({ + os: values.os, + field: entry.field, + type: entry.type, + value: entry.value, + }); + if (!entry.field || !entry.value.trim()) { isValid = false; addResultToValidation( @@ -161,9 +169,7 @@ const validateFormValues = (values: MaybeImmutable): ValidationRe values: { row: index + 1 }, }) ); - } else if ( - !isPathValid({ os: values.os, field: entry.field, type: entry.type, value: entry.value }) - ) { + } else if (!isValidPathEntry) { addResultToValidation( validation, 'entries', @@ -173,6 +179,22 @@ const validateFormValues = (values: MaybeImmutable): ValidationRe values: { row: index + 1 }, }) ); + } else if ( + isValidPathEntry && + !hasSimpleExecutableName({ os: values.os, value: entry.value, type: entry.type }) + ) { + addResultToValidation( + validation, + 'entries', + 'warnings', + i18n.translate( + 'xpack.securitySolution.trustedapps.create.conditionFieldDegradedPerformanceMsg', + { + defaultMessage: `[{row}] A wildcard in the filename will affect endpoint's performance`, + values: { row: index + 1 }, + } + ) + ); } }); } diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index e26a2c7f4b4bc..59d00d4d61d52 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -19,14 +19,18 @@ import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, } from '@kbn/securitysolution-list-constants'; +import { OperatingSystem } from '../../../../common/endpoint/types'; import { ExceptionListClient } from '../../../../../lists/server'; import { InternalArtifactCompleteSchema, TranslatedEntry, + TranslatedPerformantEntries, + translatedPerformantEntries as translatedPerformantEntriesType, translatedEntry as translatedEntryType, translatedEntryMatchAnyMatcher, TranslatedEntryMatcher, translatedEntryMatchMatcher, + TranslatedEntryMatchWildcard, TranslatedEntryMatchWildcardMatcher, translatedEntryMatchWildcardMatcher, TranslatedEntryNestedEntry, @@ -35,6 +39,10 @@ import { WrappedTranslatedExceptionList, wrappedTranslatedExceptionList, } from '../../schemas'; +import { + hasSimpleExecutableName, + getExecutableName, +} from '../../../../common/endpoint/service/trusted_apps/validations'; export async function buildArtifact( exceptions: WrappedTranslatedExceptionList, @@ -217,31 +225,84 @@ function translateItem( item: ExceptionListItemSchema ): TranslatedExceptionListItem { const itemSet = new Set(); - return { - type: item.type, - entries: item.entries.reduce((translatedEntries, entry) => { - const translatedEntry = translateEntry(schemaVersion, entry); - if (translatedEntry !== undefined && translatedEntryType.is(translatedEntry)) { - const itemHash = createHash('sha256').update(JSON.stringify(translatedEntry)).digest('hex'); - if (!itemSet.has(itemHash)) { - translatedEntries.push(translatedEntry); - itemSet.add(itemHash); + const getEntries = (): TranslatedExceptionListItem['entries'] => { + return item.entries.reduce((translatedEntries, entry) => { + const translatedEntry = translateEntry(schemaVersion, entry, item.os_types[0]); + + if (translatedEntry !== undefined) { + if (translatedEntryType.is(translatedEntry)) { + const itemHash = createHash('sha256') + .update(JSON.stringify(translatedEntry)) + .digest('hex'); + if (!itemSet.has(itemHash)) { + translatedEntries.push(translatedEntry); + itemSet.add(itemHash); + } + } + if (translatedPerformantEntriesType.is(translatedEntry)) { + translatedEntry.forEach((tpe) => { + const itemHash = createHash('sha256').update(JSON.stringify(tpe)).digest('hex'); + if (!itemSet.has(itemHash)) { + translatedEntries.push(tpe); + itemSet.add(itemHash); + } + }); } } + return translatedEntries; - }, []), + }, []); + }; + + return { + type: item.type, + entries: getEntries(), + }; +} + +function appendProcessNameEntry({ + wildcardProcessEntry, + entry, + os, +}: { + wildcardProcessEntry: TranslatedEntryMatchWildcard; + entry: { + field: string; + operator: 'excluded' | 'included'; + type: 'wildcard'; + value: string; }; + os: ExceptionListItemSchema['os_types'][0]; +}): TranslatedPerformantEntries { + const entries: TranslatedPerformantEntries = [ + wildcardProcessEntry, + { + field: normalizeFieldName('process.name'), + operator: entry.operator, + type: (os === 'linux' ? 'exact_cased' : 'exact_caseless') as Extract< + TranslatedEntryMatcher, + 'exact_caseless' | 'exact_cased' + >, + value: getExecutableName({ os: os as OperatingSystem, value: entry.value }), + }, + ].reduce((p, c) => { + p.push(c); + return p; + }, []); + + return entries; } function translateEntry( schemaVersion: string, - entry: Entry | EntryNested -): TranslatedEntry | undefined { + entry: Entry | EntryNested, + os: ExceptionListItemSchema['os_types'][0] +): TranslatedEntry | TranslatedPerformantEntries | undefined { switch (entry.type) { case 'nested': { const nestedEntries = entry.entries.reduce( (entries, nestedEntry) => { - const translatedEntry = translateEntry(schemaVersion, nestedEntry); + const translatedEntry = translateEntry(schemaVersion, nestedEntry, os); if (nestedEntry !== undefined && translatedEntryNestedEntry.is(translatedEntry)) { entries.push(translatedEntry); } @@ -278,15 +339,37 @@ function translateEntry( : undefined; } case 'wildcard': { - const matcher = getMatcherWildcardFunction(entry.field); - return translatedEntryMatchWildcardMatcher.is(matcher) - ? { + const wildcardMatcher = getMatcherWildcardFunction(entry.field); + const translatedEntryWildcardMatcher = + translatedEntryMatchWildcardMatcher.is(wildcardMatcher); + + const buildEntries = () => { + if (translatedEntryWildcardMatcher) { + // default process.executable entry + const wildcardProcessEntry: TranslatedEntryMatchWildcard = { field: normalizeFieldName(entry.field), operator: entry.operator, - type: matcher, + type: wildcardMatcher, value: entry.value, + }; + + const hasExecutableName = hasSimpleExecutableName({ + os: os as OperatingSystem, + type: entry.type, + value: entry.value, + }); + if (hasExecutableName) { + // when path has a full executable name + // append a process.name entry based on os + // `exact_cased` for linux and `exact_caseless` for others + return appendProcessNameEntry({ entry, os, wildcardProcessEntry }); + } else { + return wildcardProcessEntry; } - : undefined; + } + }; + + return buildEntries(); } } } diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts index 3a37bfbe9320c..7703304e3ae5c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts @@ -48,6 +48,24 @@ export const translatedEntryMatchWildcard = t.exact( ); export type TranslatedEntryMatchWildcard = t.TypeOf; +export const translatedEntryMatchWildcardNameMatcher = t.keyof({ + exact_cased: null, + exact_caseless: null, +}); +export type TranslatedEntryMatchWildcardNameMatcher = t.TypeOf< + typeof translatedEntryMatchWildcardNameMatcher +>; + +export const translatedEntryMatchWildcardName = t.exact( + t.type({ + field: t.string, + operator, + type: translatedEntryMatchWildcardNameMatcher, + value: t.string, + }) +); +export type TranslatedEntryMatchWildcardName = t.TypeOf; + export const translatedEntryMatch = t.exact( t.type({ field: t.string, @@ -84,6 +102,12 @@ export const translatedEntry = t.union([ ]); export type TranslatedEntry = t.TypeOf; +export const translatedPerformantEntries = t.array( + t.union([translatedEntryMatchWildcard, translatedEntryMatchWildcardName]) +); + +export type TranslatedPerformantEntries = t.TypeOf; + export const translatedExceptionListItem = t.exact( t.type({ type: t.string,