diff --git a/api_docs/security_solution.json b/api_docs/security_solution.json index aea50fdbfecaa..1e932a807d7d6 100644 --- a/api_docs/security_solution.json +++ b/api_docs/security_solution.json @@ -207,7 +207,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/public/plugin.tsx", - "lineNumber": 353 + "lineNumber": 346 } }, { @@ -221,7 +221,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/public/plugin.tsx", - "lineNumber": 353 + "lineNumber": 346 } } ], @@ -229,7 +229,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/security_solution/public/plugin.tsx", - "lineNumber": 353 + "lineNumber": 346 } }, { @@ -245,7 +245,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/security_solution/public/plugin.tsx", - "lineNumber": 398 + "lineNumber": 391 } } ], @@ -276,7 +276,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/public/types.ts", - "lineNumber": 69 + "lineNumber": 68 }, "signature": [ "() => Promise<", @@ -287,7 +287,7 @@ ], "source": { "path": "x-pack/plugins/security_solution/public/types.ts", - "lineNumber": 68 + "lineNumber": 67 }, "lifecycle": "setup", "initialIsOpen": true @@ -301,7 +301,7 @@ "children": [], "source": { "path": "x-pack/plugins/security_solution/public/types.ts", - "lineNumber": 72 + "lineNumber": 71 }, "lifecycle": "start", "initialIsOpen": true @@ -453,7 +453,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 147 + "lineNumber": 145 } } ], @@ -461,7 +461,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 147 + "lineNumber": 145 } }, { @@ -521,7 +521,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 159 + "lineNumber": 157 } }, { @@ -535,7 +535,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 159 + "lineNumber": 157 } } ], @@ -543,7 +543,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 159 + "lineNumber": 157 } }, { @@ -582,7 +582,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 341 + "lineNumber": 338 } }, { @@ -596,7 +596,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 341 + "lineNumber": 338 } } ], @@ -604,7 +604,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 341 + "lineNumber": 338 } }, { @@ -620,13 +620,13 @@ "returnComment": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 423 + "lineNumber": 412 } } ], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 131 + "lineNumber": 129 }, "initialIsOpen": false } @@ -1484,7 +1484,7 @@ "children": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 107 + "lineNumber": 105 }, "lifecycle": "setup", "initialIsOpen": true @@ -1498,7 +1498,7 @@ "children": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 110 + "lineNumber": 108 }, "lifecycle": "start", "initialIsOpen": true diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts index 27e0fa29b1e55..177f0a4b291d5 100644 --- a/x-pack/plugins/lists/common/constants.mock.ts +++ b/x-pack/plugins/lists/common/constants.mock.ts @@ -51,6 +51,7 @@ export const OPERATOR_EXCLUDED = 'excluded'; export const ENTRY_VALUE = 'some host name'; export const MATCH = 'match'; export const MATCH_ANY = 'match_any'; +export const WILDCARD = 'wildcard'; export const MAX_IMPORT_PAYLOAD_BYTES = 9000000; export const IMPORT_BUFFER_SIZE = 1000; export const LIST = 'list'; diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index f261e4e3eefa6..7e43e7dd5f4ab 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -287,6 +287,7 @@ export enum OperatorTypeEnum { NESTED = 'nested', MATCH = 'match', MATCH_ANY = 'match_any', + WILDCARD = 'wildcard', EXISTS = 'exists', LIST = 'list', } diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_wildcard.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_wildcard.ts new file mode 100644 index 0000000000000..dfcaa963666de --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_wildcard.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 { NonEmptyString } from '../../../shared_imports'; +import { operatorIncluded } from '../../common/schemas'; + +export const endpointEntryMatchWildcard = t.exact( + t.type({ + field: NonEmptyString, + operator: operatorIncluded, + type: t.keyof({ wildcard: null }), + value: NonEmptyString, + }) +); +export type EndpointEntryMatchWildcard = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/entries.ts b/x-pack/plugins/lists/common/schemas/types/entries.ts index 277751bf1c271..26cfed568cea8 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.ts @@ -12,12 +12,28 @@ import { entriesMatch } from './entry_match'; import { entriesExists } from './entry_exists'; import { entriesList } from './entry_list'; import { entriesNested } from './entry_nested'; +import { entriesMatchWildcard } from './entry_match_wildcard'; -export const entry = t.union([entriesMatch, entriesMatchAny, entriesList, entriesExists]); +// NOTE: Type nested is not included here to denote it's non-recursive nature. +// So a nested entry is really just a collection of `Entry` types. +export const entry = t.union([ + entriesMatch, + entriesMatchAny, + entriesList, + entriesExists, + entriesMatchWildcard, +]); export type Entry = t.TypeOf; export const entriesArray = t.array( - t.union([entriesMatch, entriesMatchAny, entriesList, entriesExists, entriesNested]) + t.union([ + entriesMatch, + entriesMatchAny, + entriesList, + entriesExists, + entriesNested, + entriesMatchWildcard, + ]) ); export type EntriesArray = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts new file mode 100644 index 0000000000000..3204bbe064496 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts @@ -0,0 +1,22 @@ +/* + * 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 { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../../constants.mock'; + +import { EntryMatchWildcard } from './entry_match_wildcard'; + +export const getEntryMatchWildcardMock = (): EntryMatchWildcard => ({ + field: FIELD, + operator: OPERATOR, + type: WILDCARD, + value: ENTRY_VALUE, +}); + +export const getEntryMatchWildcardExcludeMock = (): EntryMatchWildcard => ({ + ...getEntryMatchWildcardMock(), + operator: 'excluded', +}); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.test.ts new file mode 100644 index 0000000000000..53cfc4fdff1f5 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.test.ts @@ -0,0 +1,106 @@ +/* + * 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 { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; + +import { foldLeftRight, getPaths } from '../../shared_imports'; + +import { getEntryMatchWildcardMock } from './entry_match_wildcard.mock'; +import { EntryMatchWildcard, entriesMatchWildcard } from './entry_match_wildcard'; + +describe('entriesMatchWildcard', () => { + test('it should validate an entry', () => { + const payload = getEntryMatchWildcardMock(); + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when operator is "included"', () => { + const payload = getEntryMatchWildcardMock(); + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when "operator" is "excluded"', () => { + const payload = getEntryMatchWildcardMock(); + payload.operator = 'excluded'; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when "field" is empty string', () => { + const payload: Omit & { field: string } = { + ...getEntryMatchWildcardMock(), + field: '', + }; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "value" is not string', () => { + const payload: Omit & { value: string[] } = { + ...getEntryMatchWildcardMock(), + value: ['some value'], + }; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["some value"]" supplied to "value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "value" is empty string', () => { + const payload: Omit & { value: string } = { + ...getEntryMatchWildcardMock(), + value: '', + }; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "value"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "type" is not "wildcard"', () => { + const payload: Omit & { type: string } = { + ...getEntryMatchWildcardMock(), + type: 'match', + }; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EntryMatchWildcard & { + extraKey?: string; + } = getEntryMatchWildcardMock(); + payload.extraKey = 'some value'; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryMatchWildcardMock()); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.ts new file mode 100644 index 0000000000000..14522256df354 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.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 { NonEmptyString } from '../../shared_imports'; +import { operator } from '../common/schemas'; + +export const entriesMatchWildcard = t.exact( + t.type({ + field: NonEmptyString, + operator, + type: t.keyof({ wildcard: null }), + value: NonEmptyString, + }) +); +export type EntryMatchWildcard = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/index.ts b/x-pack/plugins/lists/common/schemas/types/index.ts index 98342f3b9c153..ebe21174570cb 100644 --- a/x-pack/plugins/lists/common/schemas/types/index.ts +++ b/x-pack/plugins/lists/common/schemas/types/index.ts @@ -15,6 +15,7 @@ export * from './default_namespace'; export * from './entries'; export * from './entry_match'; export * from './entry_match_any'; +export * from './entry_match_wildcard'; export * from './entry_list'; export * from './entry_exists'; export * from './entry_nested'; diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index 286fee6de5425..8be53cb8cddbc 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -20,6 +20,7 @@ export { EntryExists, EntryMatch, EntryMatchAny, + EntryMatchWildcard, EntryNested, EntryList, EntriesArray, @@ -39,6 +40,7 @@ export { nestedEntryItem, entriesMatch, entriesMatchAny, + entriesMatchWildcard, entriesExists, entriesList, namespaceType, diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/types.ts b/x-pack/plugins/lists/public/exceptions/components/builder/types.ts index cdb4f735aa103..800f1445217b9 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/types.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/types.ts @@ -13,6 +13,7 @@ import { EntryExists, EntryMatch, EntryMatchAny, + EntryMatchWildcard, EntryNested, ExceptionListItemSchema, OperatorEnum, @@ -34,7 +35,7 @@ export interface EmptyEntry { id: string; field: string | undefined; operator: OperatorEnum; - type: OperatorTypeEnum.MATCH | OperatorTypeEnum.MATCH_ANY; + type: OperatorTypeEnum.MATCH | OperatorTypeEnum.MATCH_ANY | OperatorTypeEnum.WILDCARD; value: string | string[] | undefined; } @@ -53,6 +54,7 @@ export interface EmptyNestedEntry { entries: Array< | (EntryMatch & { id?: string }) | (EntryMatchAny & { id?: string }) + | (EntryMatchWildcard & { id?: string }) | (EntryExists & { id?: string }) >; } @@ -69,6 +71,7 @@ export type BuilderEntryNested = Omit & { entries: Array< | (EntryMatch & { id?: string }) | (EntryMatchAny & { id?: string }) + | (EntryMatchWildcard & { id?: string }) | (EntryExists & { id?: string }) >; }; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts index 326795ae55662..df0d0d7acf4c7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts @@ -247,6 +247,30 @@ describe('When invoking Trusted Apps Schema', () => { expect(() => body.validate(bodyMsg)).not.toThrow(); }); + it('should validate `entry.type` does not accept `wildcard` when field is NOT PATH', () => { + const bodyMsg = createNewTrustedApp({ + entries: [ + createConditionEntry({ + field: ConditionEntryField.HASH, + type: 'wildcard', + }), + ], + }); + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + it('should validate `entry.type` accepts `wildcard` when field is PATH', () => { + const bodyMsg = createNewTrustedApp({ + entries: [ + createConditionEntry({ + field: ConditionEntryField.PATH, + type: 'wildcard', + }), + ], + }); + expect(() => body.validate(bodyMsg)).not.toThrow(); + }); + it('should validate `entry.value` required', () => { const { value, ...entry } = createConditionEntry(); expect(() => body.validate(createNewTrustedApp({ entries: [entry] }))).toThrow(); 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 e582744e1a141..54d0becd2446e 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 @@ -29,7 +29,12 @@ export const GetTrustedAppsRequestSchema = { }), }; -const ConditionEntryTypeSchema = schema.literal('match'); +const ConditionEntryTypeSchema = schema.conditional( + schema.siblingRef('field'), + ConditionEntryField.PATH, + schema.oneOf([schema.literal('match'), schema.literal('wildcard')]), + schema.literal('match') +); const ConditionEntryOperatorSchema = schema.literal('included'); /* 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 d36958c11d2a1..8d66370fea4d3 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 @@ -7,6 +7,7 @@ import { TypeOf } from '@kbn/config-schema'; import { ApplicationStart } from 'kibana/public'; + import { DeleteTrustedAppsRequestSchema, GetOneTrustedAppRequestSchema, @@ -69,9 +70,15 @@ export enum ConditionEntryField { SIGNER = 'process.Ext.code_signature', } +export enum OperatorFieldIds { + is = 'is', + matches = 'matches', +} + +export type TrustedAppEntryTypes = 'match' | 'wildcard'; export interface ConditionEntry { field: T; - type: 'match'; + type: TrustedAppEntryTypes; operator: 'included'; value: string; } diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index 033df0df6c458..e987775a8e768 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -20,6 +20,7 @@ export { EntryExists, EntryMatch, EntryMatchAny, + EntryMatchWildcard, EntryNested, EntryList, EntriesArray, @@ -38,6 +39,7 @@ export { nestedEntryItem, entriesMatch, entriesMatchAny, + entriesMatchWildcard, entriesExists, entriesList, namespaceType, diff --git a/x-pack/plugins/security_solution/common/utils/path_placeholder.test.ts b/x-pack/plugins/security_solution/common/utils/path_placeholder.test.ts new file mode 100644 index 0000000000000..9618440c105dc --- /dev/null +++ b/x-pack/plugins/security_solution/common/utils/path_placeholder.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { getPlaceholderTextByOSType, getPlaceholderText } from './path_placeholder'; +import { ConditionEntryField, OperatingSystem, TrustedAppEntryTypes } from '../endpoint/types'; + +const trustedAppEntry = { + os: OperatingSystem.LINUX, + field: ConditionEntryField.HASH, + type: 'match' as TrustedAppEntryTypes, +}; + +describe('Trusted Apps: Path placeholder text', () => { + it('returns no placeholder text when field IS NOT PATH', () => { + expect(getPlaceholderTextByOSType({ ...trustedAppEntry })).toEqual(undefined); + }); + + it('returns a placeholder text when field IS PATH', () => { + expect( + getPlaceholderTextByOSType({ ...trustedAppEntry, field: ConditionEntryField.PATH }) + ).toEqual(getPlaceholderText().others.exact); + }); + + it('returns LINUX/MAC equivalent placeholder when field IS PATH', () => { + expect( + getPlaceholderTextByOSType({ + ...trustedAppEntry, + os: OperatingSystem.MAC, + field: ConditionEntryField.PATH, + }) + ).toEqual(getPlaceholderText().others.exact); + }); + + it('returns LINUX/MAC equivalent placeholder text when field IS PATH and WILDCARD operator is selected', () => { + expect( + getPlaceholderTextByOSType({ + ...trustedAppEntry, + os: OperatingSystem.LINUX, + field: ConditionEntryField.PATH, + type: 'wildcard', + }) + ).toEqual(getPlaceholderText().others.wildcard); + }); + + it('returns WINDOWS equivalent placeholder text when field IS PATH', () => { + expect( + getPlaceholderTextByOSType({ + ...trustedAppEntry, + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + }) + ).toEqual(getPlaceholderText().windows.exact); + }); + + it('returns WINDOWS equivalent placeholder text when field IS PATH and WILDCARD operator is selected', () => { + expect( + getPlaceholderTextByOSType({ + ...trustedAppEntry, + os: OperatingSystem.WINDOWS, + field: ConditionEntryField.PATH, + type: 'wildcard', + }) + ).toEqual(getPlaceholderText().windows.wildcard); + }); +}); diff --git a/x-pack/plugins/security_solution/common/utils/path_placeholder.ts b/x-pack/plugins/security_solution/common/utils/path_placeholder.ts new file mode 100644 index 0000000000000..bba01b6d05b65 --- /dev/null +++ b/x-pack/plugins/security_solution/common/utils/path_placeholder.ts @@ -0,0 +1,43 @@ +/* + * 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 { ConditionEntryField, OperatingSystem, TrustedAppEntryTypes } from '../endpoint/types'; + +export const getPlaceholderText = () => ({ + windows: { + wildcard: 'C:\\sample\\**\\*', + exact: 'C:\\sample\\path.exe', + }, + others: { + wildcard: '/opt/**/*', + exact: '/opt/bin', + }, +}); + +export const getPlaceholderTextByOSType = ({ + os, + field, + type, +}: { + os: OperatingSystem; + field: ConditionEntryField; + type: TrustedAppEntryTypes; +}): string | undefined => { + if (field === ConditionEntryField.PATH) { + if (os === OperatingSystem.WINDOWS) { + if (type === 'wildcard') { + return getPlaceholderText().windows.wildcard; + } + return getPlaceholderText().windows.exact; + } else { + if (type === 'wildcard') { + return getPlaceholderText().others.wildcard; + } + return getPlaceholderText().others.exact; + } + } +}; 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 c7a125daa54f8..92a3cb2cfac93 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 @@ -15,6 +15,7 @@ import { Entry, EntryMatch, EntryMatchAny, + EntryMatchWildcard, EntryExists, ExceptionListItemSchema, CreateExceptionListItemSchema, @@ -92,6 +93,7 @@ export interface EmptyNestedEntry { type: OperatorTypeEnum.NESTED; entries: Array< | (EntryMatch & { id?: string }) + | (EntryMatchWildcard & { id?: string }) | (EntryMatchAny & { id?: string }) | (EntryExists & { id?: string }) >; @@ -108,6 +110,7 @@ export type BuilderEntryNested = Omit & { id?: string; entries: Array< | (EntryMatch & { id?: string }) + | (EntryMatchWildcard & { id?: string }) | (EntryMatchAny & { id?: string }) | (EntryExists & { id?: string }) >; 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 4e9ec3a0883a2..9d6c35d64b2d5 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 @@ -21,7 +21,7 @@ let onRemoveMock: jest.Mock; let onChangeMock: jest.Mock; let onVisitedMock: jest.Mock; -const entry: Readonly = { +const baseEntry: Readonly = { field: ConditionEntryField.HASH, type: 'match', operator: 'included', @@ -38,7 +38,8 @@ describe('Condition entry input', () => { const getElement = ( subject: string, os: OperatingSystem = OperatingSystem.WINDOWS, - isRemoveDisabled: boolean = false + isRemoveDisabled: boolean = false, + entry: ConditionEntry = baseEntry ) => ( { expect(onChangeMock).toHaveBeenCalledTimes(1); expect(onChangeMock).toHaveBeenCalledWith( { - ...entry, + ...baseEntry, field: { target: { value: field } }, }, - entry + baseEntry ); } ); @@ -77,7 +78,7 @@ describe('Condition entry input', () => { expect(onRemoveMock).toHaveBeenCalledTimes(0); element.find('[data-test-subj="testOnRemove-remove"]').first().simulate('click'); expect(onRemoveMock).toHaveBeenCalledTimes(1); - expect(onRemoveMock).toHaveBeenCalledWith(entry); + expect(onRemoveMock).toHaveBeenCalledWith(baseEntry); }); it('should not be able to call on remove for field input because disabled', () => { @@ -92,7 +93,7 @@ describe('Condition entry input', () => { expect(onVisitedMock).toHaveBeenCalledTimes(0); element.find('[data-test-subj="testOnVisited-value"]').first().simulate('blur'); expect(onVisitedMock).toHaveBeenCalledTimes(1); - expect(onVisitedMock).toHaveBeenCalledWith(entry); + expect(onVisitedMock).toHaveBeenCalledWith(baseEntry); }); it('should change value for field input', () => { @@ -105,10 +106,10 @@ describe('Condition entry input', () => { expect(onChangeMock).toHaveBeenCalledTimes(1); expect(onChangeMock).toHaveBeenCalledWith( { - ...entry, + ...baseEntry, value: 'new value', }, - entry + baseEntry ); }); @@ -138,4 +139,24 @@ describe('Condition entry input', () => { .props() as EuiSuperSelectProps; expect(superSelectProps.options.length).toBe(2); }); + + it('should have operator value selected when field is HASH', () => { + const element = shallow(getElement('testOperatorOptions')); + const inputField = element.find('[data-test-subj="testOperatorOptions-operator"]'); + expect(inputField.contains('is')); + }); + + it('should show operator dorpdown with two values when field is PATH', () => { + const element = shallow( + getElement('testOperatorOptions', undefined, undefined, { + ...baseEntry, + field: ConditionEntryField.PATH, + }) + ); + const superSelectProps = element + .find('[data-test-subj="testOperatorOptions-operator"]') + .first() + .props() as EuiSuperSelectProps; + expect(superSelectProps.options.length).toBe(2); + }); }); 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 633adde4fdfbb..d052138d309ac 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 @@ -6,12 +6,11 @@ */ import React, { ChangeEventHandler, memo, useCallback, useMemo } from 'react'; +import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiFieldText, - EuiFlexGroup, - EuiFlexItem, EuiFormRow, EuiSuperSelect, EuiSuperSelectOption, @@ -21,6 +20,7 @@ import { import { ConditionEntry, ConditionEntryField, + OperatorFieldIds, OperatingSystem, } from '../../../../../../../common/endpoint/types'; @@ -28,9 +28,10 @@ import { CONDITION_FIELD_DESCRIPTION, CONDITION_FIELD_TITLE, ENTRY_PROPERTY_TITLES, - OPERATOR_TITLE, + OPERATOR_TITLES, } from '../../translations'; import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator'; +import { getPlaceholderTextByOSType } from '../../../../../../../common/utils/path_placeholder'; const ConditionEntryCell = memo<{ showLabel: boolean; @@ -66,6 +67,27 @@ export interface ConditionEntryInputProps { 'data-test-subj'?: string; } +// adding a style prop on EuiFlexGroup works only partially +// and for some odd reason garbles up gridTemplateAreas entry +const InputGroup = styled.div` + display: grid; + grid-template-columns: 25% 25% 45% 5%; + grid-template-areas: 'field operator value remove'; +`; + +const InputItem = styled.div<{ gridArea: string }>` + grid-area: ${({ gridArea }) => gridArea}; + align-self: center; + margin: 4px; + vertical-align: baseline; +`; + +const operatorOptions = (Object.keys(OperatorFieldIds) as OperatorFieldIds[]).map((value) => ({ + dropdownDisplay: OPERATOR_TITLES[value], + inputDisplay: OPERATOR_TITLES[value], + value: value === 'matches' ? 'wildcard' : 'match', +})); + export const ConditionEntryInput = memo( ({ os, @@ -122,6 +144,11 @@ export const ConditionEntryInput = memo( [entry, onChange] ); + const handleOperatorUpdate = useCallback( + (newOperator) => onChange({ ...entry, type: newOperator }, entry), + [entry, onChange] + ); + const handleRemoveClick = useCallback(() => onRemove(entry), [entry, onRemove]); const handleValueOnBlur = useCallback(() => { @@ -131,14 +158,8 @@ export const ConditionEntryInput = memo( }, [entry, onVisited]); return ( - - + + ( data-test-subj={getTestId('field')} /> - - + + - + {entry.field === ConditionEntryField.PATH ? ( + + ) : ( + + )} - - + + ( data-test-subj={getTestId('value')} /> - - + + {/* Unicode `nbsp` is used below so that Remove button is property displayed */} ( data-test-subj={getTestId('remove')} /> - - + + ); } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.tsx index 0520f760d7343..8289792b81f89 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.tsx @@ -31,7 +31,7 @@ import { ENTRY_PROPERTY_TITLES, CARD_DELETE_BUTTON_LABEL, CONDITION_FIELD_TITLE, - OPERATOR_TITLE, + OPERATOR_TITLES, CARD_EDIT_BUTTON_LABEL, } from '../../translations'; @@ -45,7 +45,7 @@ const getEntriesColumnDefinitions = (): Array truncateText: true, textOnly: true, width: '30%', - render(field: Entry['field'], entry: Entry) { + render(field: Entry['field'], _entry: Entry) { return CONDITION_FIELD_TITLE[field]; }, }, @@ -55,8 +55,8 @@ const getEntriesColumnDefinitions = (): Array sortable: false, truncateText: true, width: '20%', - render(field: Entry['operator'], entry: Entry) { - return OPERATOR_TITLE[field]; + render(_field: Entry['operator'], entry: Entry) { + return entry.type === 'wildcard' ? OPERATOR_TITLES.matches : OPERATOR_TITLES.is; }, }, { 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 c3e2a372fd6dc..fc031a63b84b1 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 @@ -10,8 +10,8 @@ import { TrustedApp, MacosLinuxConditionEntry, WindowsConditionEntry, - ConditionEntry, ConditionEntryField, + OperatorFieldIds, } from '../../../../../common/endpoint/types'; export { OS_TITLES } from '../../../common/translations'; @@ -52,10 +52,13 @@ export const CONDITION_FIELD_DESCRIPTION: { [K in ConditionEntryField]: string } ), }; -export const OPERATOR_TITLE: { [K in ConditionEntry['operator']]: string } = { - included: i18n.translate('xpack.securitySolution.trustedapps.card.operator.includes', { +export const OPERATOR_TITLES: { [K in OperatorFieldIds]: string } = { + is: i18n.translate('xpack.securitySolution.trustedapps.card.operator.is', { defaultMessage: 'is', }), + matches: i18n.translate('xpack.securitySolution.trustedapps.card.operator.matches', { + defaultMessage: 'matches', + }), }; export const PROPERTY_TITLES: Readonly< 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 1c3c92c50afd3..54b6971eec58e 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 @@ -22,6 +22,8 @@ import { translatedEntryMatchAnyMatcher, TranslatedEntryMatcher, translatedEntryMatchMatcher, + TranslatedEntryMatchWildcardMatcher, + translatedEntryMatchWildcardMatcher, TranslatedEntryNestedEntry, translatedEntryNestedEntry, TranslatedExceptionListItem, @@ -203,6 +205,10 @@ function getMatcherFunction(field: string, matchAny?: boolean): TranslatedEntryM : 'exact_cased'; } +function getMatcherWildcardFunction(field: string): TranslatedEntryMatchWildcardMatcher { + return field.endsWith('.caseless') ? 'wildcard_caseless' : 'wildcard_cased'; +} + function normalizeFieldName(field: string): string { return field.endsWith('.caseless') ? field.substring(0, field.lastIndexOf('.')) : field; } @@ -272,6 +278,17 @@ function translateEntry( } : undefined; } + case 'wildcard': { + const matcher = getMatcherWildcardFunction(entry.field); + return translatedEntryMatchWildcardMatcher.is(matcher) + ? { + field: normalizeFieldName(entry.field), + operator: entry.operator, + type: matcher, + value: entry.value, + } + : undefined; + } } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts index 42a2e0f43d970..0b4e1cb2b09b1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts @@ -66,8 +66,8 @@ const NEW_TRUSTED_APP: NewTrustedApp = { os: OperatingSystem.LINUX, effectScope: { type: 'global' }, entries: [ - createConditionEntry(ConditionEntryField.PATH, '/bin/malware'), - createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'), + createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware'), + createConditionEntry(ConditionEntryField.HASH, 'match', '1234234659af249ddf3e40864e9fb241'), ], }; @@ -83,8 +83,8 @@ const TRUSTED_APP: TrustedApp = { os: OperatingSystem.LINUX, effectScope: { type: 'global' }, entries: [ - createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'), - createConditionEntry(ConditionEntryField.PATH, '/bin/malware'), + createConditionEntry(ConditionEntryField.HASH, 'match', '1234234659af249ddf3e40864e9fb241'), + createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware'), ], }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts index 68ff7d03e413a..9ee2ece627841 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts @@ -79,13 +79,19 @@ describe('mapping', () => { description: 'Linux Trusted App', effectScope: { type: 'global' }, os: OperatingSystem.LINUX, - entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')], + entries: [createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware')], }, createExceptionListItemOptions({ name: 'linux trusted app', description: 'Linux Trusted App', osTypes: ['linux'], - entries: [createEntryMatch('process.executable.caseless', '/bin/malware')], + entries: [ + createEntryMatch( + 'process.executable.caseless', + + '/bin/malware' + ), + ], }) ); }); @@ -97,13 +103,19 @@ describe('mapping', () => { description: 'MacOS Trusted App', effectScope: { type: 'global' }, os: OperatingSystem.MAC, - entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')], + entries: [createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware')], }, createExceptionListItemOptions({ name: 'macos trusted app', description: 'MacOS Trusted App', osTypes: ['macos'], - entries: [createEntryMatch('process.executable.caseless', '/bin/malware')], + entries: [ + createEntryMatch( + 'process.executable.caseless', + + '/bin/malware' + ), + ], }) ); }); @@ -115,13 +127,21 @@ describe('mapping', () => { description: 'Windows Trusted App', effectScope: { type: 'global' }, os: OperatingSystem.WINDOWS, - entries: [createConditionEntry(ConditionEntryField.PATH, 'C:\\Program Files\\Malware')], + entries: [ + createConditionEntry(ConditionEntryField.PATH, 'match', 'C:\\Program Files\\Malware'), + ], }, createExceptionListItemOptions({ name: 'windows trusted app', description: 'Windows Trusted App', osTypes: ['windows'], - entries: [createEntryMatch('process.executable.caseless', 'C:\\Program Files\\Malware')], + entries: [ + createEntryMatch( + 'process.executable.caseless', + + 'C:\\Program Files\\Malware' + ), + ], }) ); }); @@ -133,7 +153,7 @@ describe('mapping', () => { description: 'Signed Trusted App', effectScope: { type: 'global' }, os: OperatingSystem.WINDOWS, - entries: [createConditionEntry(ConditionEntryField.SIGNER, 'Microsoft Windows')], + entries: [createConditionEntry(ConditionEntryField.SIGNER, 'match', 'Microsoft Windows')], }, createExceptionListItemOptions({ name: 'Signed trusted app', @@ -157,14 +177,24 @@ describe('mapping', () => { effectScope: { type: 'global' }, os: OperatingSystem.LINUX, entries: [ - createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'), + createConditionEntry( + ConditionEntryField.HASH, + 'match', + '1234234659af249ddf3e40864e9fb241' + ), ], }, createExceptionListItemOptions({ name: 'MD5 trusted app', description: 'MD5 Trusted App', osTypes: ['linux'], - entries: [createEntryMatch('process.hash.md5', '1234234659af249ddf3e40864e9fb241')], + entries: [ + createEntryMatch( + 'process.hash.md5', + + '1234234659af249ddf3e40864e9fb241' + ), + ], }) ); }); @@ -179,6 +209,7 @@ describe('mapping', () => { entries: [ createConditionEntry( ConditionEntryField.HASH, + 'match', 'f635da961234234659af249ddf3e40864e9fb241' ), ], @@ -188,7 +219,11 @@ describe('mapping', () => { description: 'SHA1 Trusted App', osTypes: ['linux'], entries: [ - createEntryMatch('process.hash.sha1', 'f635da961234234659af249ddf3e40864e9fb241'), + createEntryMatch( + 'process.hash.sha1', + + 'f635da961234234659af249ddf3e40864e9fb241' + ), ], }) ); @@ -204,6 +239,7 @@ describe('mapping', () => { entries: [ createConditionEntry( ConditionEntryField.HASH, + 'match', 'f635da96124659af249ddf3e40864e9fb234234659af249ddf3e40864e9fb241' ), ], @@ -215,6 +251,7 @@ describe('mapping', () => { entries: [ createEntryMatch( 'process.hash.sha256', + 'f635da96124659af249ddf3e40864e9fb234234659af249ddf3e40864e9fb241' ), ], @@ -230,14 +267,24 @@ describe('mapping', () => { effectScope: { type: 'global' }, os: OperatingSystem.LINUX, entries: [ - createConditionEntry(ConditionEntryField.HASH, '1234234659Af249ddf3e40864E9FB241'), + createConditionEntry( + ConditionEntryField.HASH, + 'match', + '1234234659Af249ddf3e40864E9FB241' + ), ], }, createExceptionListItemOptions({ name: 'MD5 trusted app', description: 'MD5 Trusted App', osTypes: ['linux'], - entries: [createEntryMatch('process.hash.md5', '1234234659af249ddf3e40864e9fb241')], + entries: [ + createEntryMatch( + 'process.hash.md5', + + '1234234659af249ddf3e40864e9fb241' + ), + ], }) ); }); @@ -257,7 +304,13 @@ describe('mapping', () => { created_at: '11/11/2011T11:11:11.111', created_by: 'admin', os_types: ['linux'], - entries: [createEntryMatch('process.executable.caseless', '/bin/malware')], + entries: [ + createEntryMatch( + 'process.executable.caseless', + + '/bin/malware' + ), + ], }), { id: '123', @@ -270,7 +323,7 @@ describe('mapping', () => { updated_at: '11/11/2011T11:11:11.111', updated_by: 'admin', os: OperatingSystem.LINUX, - entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')], + entries: [createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware')], } ); }); @@ -284,7 +337,13 @@ describe('mapping', () => { created_at: '11/11/2011T11:11:11.111', created_by: 'admin', os_types: ['macos'], - entries: [createEntryMatch('process.executable.caseless', '/bin/malware')], + entries: [ + createEntryMatch( + 'process.executable.caseless', + + '/bin/malware' + ), + ], }), { id: '123', @@ -297,7 +356,7 @@ describe('mapping', () => { updated_at: '11/11/2011T11:11:11.111', updated_by: 'admin', os: OperatingSystem.MAC, - entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')], + entries: [createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware')], } ); }); @@ -311,7 +370,13 @@ describe('mapping', () => { created_at: '11/11/2011T11:11:11.111', created_by: 'admin', os_types: ['windows'], - entries: [createEntryMatch('process.executable.caseless', 'C:\\Program Files\\Malware')], + entries: [ + createEntryMatch( + 'process.executable.caseless', + + 'C:\\Program Files\\Malware' + ), + ], }), { id: '123', @@ -324,7 +389,9 @@ describe('mapping', () => { updated_at: '11/11/2011T11:11:11.111', updated_by: 'admin', os: OperatingSystem.WINDOWS, - entries: [createConditionEntry(ConditionEntryField.PATH, 'C:\\Program Files\\Malware')], + entries: [ + createConditionEntry(ConditionEntryField.PATH, 'match', 'C:\\Program Files\\Malware'), + ], } ); }); @@ -356,7 +423,7 @@ describe('mapping', () => { updated_at: '11/11/2011T11:11:11.111', updated_by: 'admin', os: OperatingSystem.WINDOWS, - entries: [createConditionEntry(ConditionEntryField.SIGNER, 'Microsoft Windows')], + entries: [createConditionEntry(ConditionEntryField.SIGNER, 'match', 'Microsoft Windows')], } ); }); @@ -370,7 +437,13 @@ describe('mapping', () => { created_at: '11/11/2011T11:11:11.111', created_by: 'admin', os_types: ['linux'], - entries: [createEntryMatch('process.hash.md5', '1234234659af249ddf3e40864e9fb241')], + entries: [ + createEntryMatch( + 'process.hash.md5', + + '1234234659af249ddf3e40864e9fb241' + ), + ], }), { id: '123', @@ -384,7 +457,11 @@ describe('mapping', () => { updated_by: 'admin', os: OperatingSystem.LINUX, entries: [ - createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'), + createConditionEntry( + ConditionEntryField.HASH, + 'match', + '1234234659af249ddf3e40864e9fb241' + ), ], } ); @@ -400,7 +477,11 @@ describe('mapping', () => { created_by: 'admin', os_types: ['linux'], entries: [ - createEntryMatch('process.hash.sha1', 'f635da961234234659af249ddf3e40864e9fb241'), + createEntryMatch( + 'process.hash.sha1', + + 'f635da961234234659af249ddf3e40864e9fb241' + ), ], }), { @@ -417,6 +498,7 @@ describe('mapping', () => { entries: [ createConditionEntry( ConditionEntryField.HASH, + 'match', 'f635da961234234659af249ddf3e40864e9fb241' ), ], @@ -436,6 +518,7 @@ describe('mapping', () => { entries: [ createEntryMatch( 'process.hash.sha256', + 'f635da96124659af249ddf3e40864e9fb234234659af249ddf3e40864e9fb241' ), ], @@ -454,6 +537,7 @@ describe('mapping', () => { entries: [ createConditionEntry( ConditionEntryField.HASH, + 'match', 'f635da96124659af249ddf3e40864e9fb234234659af249ddf3e40864e9fb241' ), ], @@ -469,7 +553,7 @@ describe('mapping', () => { description: 'Linux Trusted App', effectScope: { type: 'global' }, os: OperatingSystem.LINUX, - entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')], + entries: [createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware')], version: 'abc', }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts index c6048e5725c88..786a74e91b51a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts @@ -11,6 +11,7 @@ import { OsType } from '../../../../../lists/common/schemas'; import { EntriesArray, EntryMatch, + EntryMatchWildcard, EntryNested, ExceptionListItemSchema, NestedEntriesArray, @@ -28,6 +29,7 @@ import { OperatingSystem, TrustedApp, UpdateTrustedApp, + TrustedAppEntryTypes, } from '../../../../common/endpoint/types'; type ConditionEntriesMap = { [K in ConditionEntryField]?: ConditionEntry }; @@ -46,6 +48,7 @@ const OPERATING_SYSTEM_TO_OS_TYPE: Mapping = { }; const POLICY_REFERENCE_PREFIX = 'policy:'; +const OPERATOR_VALUE = 'included'; const filterUndefined = (list: Array): T[] => { return list.filter((item: T | undefined): item is T => item !== undefined); @@ -53,9 +56,10 @@ const filterUndefined = (list: Array): T[] => { export const createConditionEntry = ( field: T, + type: TrustedAppEntryTypes, value: string ): ConditionEntry => { - return { field, value, type: 'match', operator: 'included' }; + return { field, value, type, operator: OPERATOR_VALUE }; }; export const tagsToEffectScope = (tags: string[]): EffectScope => { @@ -78,12 +82,23 @@ export const entriesToConditionEntriesMap = (entries: EntriesArray): ConditionEn if (entry.field.startsWith('process.hash') && entry.type === 'match') { return { ...result, - [ConditionEntryField.HASH]: createConditionEntry(ConditionEntryField.HASH, entry.value), + [ConditionEntryField.HASH]: createConditionEntry( + ConditionEntryField.HASH, + entry.type, + entry.value + ), }; - } else if (entry.field === 'process.executable.caseless' && entry.type === 'match') { + } else if ( + entry.field === 'process.executable.caseless' && + (entry.type === 'match' || entry.type === 'wildcard') + ) { return { ...result, - [ConditionEntryField.PATH]: createConditionEntry(ConditionEntryField.PATH, entry.value), + [ConditionEntryField.PATH]: createConditionEntry( + ConditionEntryField.PATH, + entry.type, + entry.value + ), }; } else if (entry.field === 'process.Ext.code_signature' && entry.type === 'nested') { const subjectNameCondition = entry.entries.find((subEntry): subEntry is EntryMatch => { @@ -95,6 +110,7 @@ export const entriesToConditionEntriesMap = (entries: EntriesArray): ConditionEn ...result, [ConditionEntryField.SIGNER]: createConditionEntry( ConditionEntryField.SIGNER, + subjectNameCondition.type, subjectNameCondition.value ), }; @@ -166,7 +182,11 @@ const hashType = (hash: string): 'md5' | 'sha256' | 'sha1' | undefined => { }; export const createEntryMatch = (field: string, value: string): EntryMatch => { - return { field, value, type: 'match', operator: 'included' }; + return { field, value, type: 'match', operator: OPERATOR_VALUE }; +}; + +export const createEntryMatchWildcard = (field: string, value: string): EntryMatchWildcard => { + return { field, value, type: 'wildcard', operator: OPERATOR_VALUE }; }; export const createEntryNested = (field: string, entries: NestedEntriesArray): EntryNested => { @@ -193,6 +213,11 @@ export const conditionEntriesToEntries = (conditionEntries: ConditionEntry[]): E createEntryMatch('trusted', 'true'), createEntryMatch('subject_name', conditionEntry.value), ]); + } else if ( + conditionEntry.field === ConditionEntryField.PATH && + conditionEntry.type === 'wildcard' + ) { + return createEntryMatchWildcard(`process.executable.caseless`, conditionEntry.value); } else { return createEntryMatch(`process.executable.caseless`, conditionEntry.value); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts index 42f4c6d157389..d99a89ce11137 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts @@ -65,8 +65,8 @@ const TRUSTED_APP: TrustedApp = { os: OperatingSystem.LINUX, effectScope: { type: 'global' }, entries: [ - createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'), - createConditionEntry(ConditionEntryField.PATH, '/bin/malware'), + createConditionEntry(ConditionEntryField.HASH, 'match', '1234234659af249ddf3e40864e9fb241'), + createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware'), ], }; @@ -109,8 +109,35 @@ describe('service', () => { effectScope: { type: 'global' }, os: OperatingSystem.LINUX, entries: [ - createConditionEntry(ConditionEntryField.PATH, '/bin/malware'), - createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'), + createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware'), + createConditionEntry( + ConditionEntryField.HASH, + 'match', + '1234234659af249ddf3e40864e9fb241' + ), + ], + }); + + expect(result).toEqual({ data: TRUSTED_APP }); + + expect(exceptionsListClient.createTrustedAppsList).toHaveBeenCalled(); + }); + + it('should create trusted app with correct wildcard type', async () => { + exceptionsListClient.createExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); + + const result = await createTrustedApp(exceptionsListClient, { + name: 'linux trusted app 1', + description: 'Linux trusted app 1', + effectScope: { type: 'global' }, + os: OperatingSystem.LINUX, + entries: [ + createConditionEntry(ConditionEntryField.PATH, 'wildcard', '/bin/malware'), + createConditionEntry( + ConditionEntryField.HASH, + 'wildcard', + '1234234659af249ddf3e40864e9fb241' + ), ], }); 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 4c11325652f80..1b1370472f633 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 @@ -30,6 +30,24 @@ export const translatedEntryMatchMatcher = t.keyof({ }); export type TranslatedEntryMatchMatcher = t.TypeOf; +export const translatedEntryMatchWildcardMatcher = t.keyof({ + wildcard_cased: null, + wildcard_caseless: null, +}); +export type TranslatedEntryMatchWildcardMatcher = t.TypeOf< + typeof translatedEntryMatchWildcardMatcher +>; + +export const translatedEntryMatchWildcard = t.exact( + t.type({ + field: t.string, + operator, + type: translatedEntryMatchWildcardMatcher, + value: t.string, + }) +); +export type TranslatedEntryMatchWildcard = t.TypeOf; + export const translatedEntryMatch = t.exact( t.type({ field: t.string, @@ -61,6 +79,7 @@ export type TranslatedEntryNested = t.TypeOf; export const translatedEntry = t.union([ translatedEntryNested, translatedEntryMatch, + translatedEntryMatchWildcard, translatedEntryMatchAny, ]); export type TranslatedEntry = t.TypeOf; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fe9bf6eac14d0..746af100cb733 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20118,7 +20118,6 @@ "xpack.securitySolution.topN.closeButtonLabel": "閉じる", "xpack.securitySolution.topN.rawEventsSelectLabel": "未加工イベント", "xpack.securitySolution.trustedapps.aboutInfo": "パフォーマンスを改善したり、ホストで実行されている他のアプリケーションとの競合を解消したりするには、信頼できるアプリケーションを追加します。信頼できるアプリケーションは、Endpoint Securityを実行しているホストに適用されます。", - "xpack.securitySolution.trustedapps.card.operator.includes": "is", "xpack.securitySolution.trustedapps.card.removeButtonLabel": "削除", "xpack.securitySolution.trustedapps.create.conditionFieldValueRequiredMsg": "[{row}] フィールドエントリには値が必要です", "xpack.securitySolution.trustedapps.create.conditionRequiredMsg": "1つ以上のフィールド定義が必要です", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7eab0f835a556..163d9af5eeafb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20439,7 +20439,6 @@ "xpack.securitySolution.topN.closeButtonLabel": "关闭", "xpack.securitySolution.topN.rawEventsSelectLabel": "原始事件", "xpack.securitySolution.trustedapps.aboutInfo": "添加受信任的应用程序,以提高性能或缓解与主机上运行的其他应用程序的冲突。受信任的应用程序将应用于运行 Endpoint Security 的主机。", - "xpack.securitySolution.trustedapps.card.operator.includes": "是", "xpack.securitySolution.trustedapps.card.removeButtonLabel": "移除", "xpack.securitySolution.trustedapps.create.conditionFieldValueRequiredMsg": "[{row}] 字段条目必须包含值", "xpack.securitySolution.trustedapps.create.conditionRequiredMsg": "至少需要一个字段定义",