[Security Solution][Endpoint] Allow wildcard in trusted app paths (#97623) (#98749)

* show operator dropdown for path field

refs elastic/security-team/issues/543

* update translation to use consistent values

refs elastic/security-team/issues/543

* update schema to validate path values

refs elastic/security-team/issues/543

* add tests for field and operator values

refs elastic/security-team/issues/543

* review changes

refs elastic/security-team/issues/543

* update schema to enforce dropdown validation for PATH field

refs elastic/security-team/issues/543

* add tests for schema updates

refs 1deab39453
refs elastic/security-team/issues/543

* optimise dropdown list for re-renders

refs elastic/security-team/issues/543

* align input fields and keep alignments when resized

refs elastic/security-team/issues/543

* correctly enter operator data on trusted app CRUD

refs elastic/security-team/issues/543

* update tests

refs 2ac56ee839
refs elastic/security-team/issues/543

* remove redundant code

review changes

* better type assertion

review changes

* move operator options out of component

- these do not depend on component props and thus no need to have it within a useMemo callback.

- review changes

* derive keys from operator entry field

review changes

* update type

* use custom styles for aligning input fields

review changes

* add a custom type for trusted_apps operator

undo changes from list plugin and server/lib/detection_engine

refs 2ac56ee839
refs elastic/security-team/issues/543

* add wildcard entry type

refs elastic/security-team/issues/543
refs https://github.com/elastic/kibana/pull/97623#pullrequestreview-642618462

* use the new entry type

refs elastic/security-team/issues/543
refs https://github.com/elastic/kibana/pull/97623#pullrequestreview-642618462

* update tests

refs elastic/security-team/issues/543
refs https://github.com/elastic/kibana/pull/97623#pullrequestreview-642618462

* update name for wildcard type so that it can be used also for cased inputs

refs elastic/security-team/issues/543
refs f9cb7eddda

* update artifacts to support wildcard entries

refs elastic/security-team/issues/543

* add tests for list schemas

refs f9cb7eddda
refs elastic/security-team/issues/543

* add placeholders for path values

review changes
elastic/kibana/pull/97623#discussion_r620617999

* ignore type check for now

* add type assertion

refs 284352ec9a

* remove unnecessary test

refs 2ac56ee839

* fix types

refs f9cb7eddda
refs b3f5dc4553

* add a note to entries

review changes

refs dbd3532149

* remove redundant type assertions

review changes
refs bcf615ac98
refs b3f5dc4553

* move placeholder text logic to utils

review changes elastic/kibana/pull/97623#discussion_r621673881

refs 6f2d0d7810

* pass the style as prop

review changes

* update api doc

CI check suggestion

* make placeholderText a function expression

review suggestion

elastic/kibana/pull/97623/commits/2dc4fd390cf5ea0e4fa67b3f5fc2561cbb29555e

* use semantic names for functions

refs 330731ebfc

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
# Conflicts:
#	api_docs/security_solution.json
This commit is contained in:
Ashokaditya 2021-04-29 17:46:50 +02:00 committed by GitHub
parent 6ab291205f
commit ed51d3115e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 678 additions and 97 deletions

View file

@ -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": 421
"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

View file

@ -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';

View file

@ -287,6 +287,7 @@ export enum OperatorTypeEnum {
NESTED = 'nested',
MATCH = 'match',
MATCH_ANY = 'match_any',
WILDCARD = 'wildcard',
EXISTS = 'exists',
LIST = 'list',
}

View file

@ -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<typeof endpointEntryMatchWildcard>;

View file

@ -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<typeof entry>;
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<typeof entriesArray>;

View file

@ -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',
});

View file

@ -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<EntryMatchWildcard, 'field'> & { 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<EntryMatchWildcard, 'value'> & { 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<EntryMatchWildcard, 'value'> & { 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<EntryMatchWildcard, 'type'> & { 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());
});
});

View file

@ -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<typeof entriesMatchWildcard>;

View file

@ -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';

View file

@ -20,6 +20,7 @@ export {
EntryExists,
EntryMatch,
EntryMatchAny,
EntryMatchWildcard,
EntryNested,
EntryList,
EntriesArray,
@ -39,6 +40,7 @@ export {
nestedEntryItem,
entriesMatch,
entriesMatchAny,
entriesMatchWildcard,
entriesExists,
entriesList,
namespaceType,

View file

@ -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<EntryNested, 'entries'> & {
entries: Array<
| (EntryMatch & { id?: string })
| (EntryMatchAny & { id?: string })
| (EntryMatchWildcard & { id?: string })
| (EntryExists & { id?: string })
>;
};

View file

@ -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();

View file

@ -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');
/*

View file

@ -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<T extends ConditionEntryField = ConditionEntryField> {
field: T;
type: 'match';
type: TrustedAppEntryTypes;
operator: 'included';
value: string;
}

View file

@ -20,6 +20,7 @@ export {
EntryExists,
EntryMatch,
EntryMatchAny,
EntryMatchWildcard,
EntryNested,
EntryList,
EntriesArray,
@ -38,6 +39,7 @@ export {
nestedEntryItem,
entriesMatch,
entriesMatchAny,
entriesMatchWildcard,
entriesExists,
entriesList,
namespaceType,

View file

@ -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);
});
});

View file

@ -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;
}
}
};

View file

@ -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<EntryNested, 'entries'> & {
id?: string;
entries: Array<
| (EntryMatch & { id?: string })
| (EntryMatchWildcard & { id?: string })
| (EntryMatchAny & { id?: string })
| (EntryExists & { id?: string })
>;

View file

@ -21,7 +21,7 @@ let onRemoveMock: jest.Mock;
let onChangeMock: jest.Mock;
let onVisitedMock: jest.Mock;
const entry: Readonly<ConditionEntry> = {
const baseEntry: Readonly<ConditionEntry> = {
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
) => (
<ConditionEntryInput
os={os}
@ -64,10 +65,10 @@ describe('Condition entry input', () => {
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<string>;
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<string>;
expect(superSelectProps.options.length).toBe(2);
});
});

View file

@ -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<ConditionEntryInputProps>(
({
os,
@ -122,6 +144,11 @@ export const ConditionEntryInput = memo<ConditionEntryInputProps>(
[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<ConditionEntryInputProps>(
}, [entry, onVisited]);
return (
<EuiFlexGroup
gutterSize="s"
alignItems="center"
direction="row"
data-test-subj={dataTestSubj}
responsive={false}
>
<EuiFlexItem grow={2}>
<InputGroup data-test-subj={dataTestSubj}>
<InputItem gridArea="field">
<ConditionEntryCell showLabel={showLabels} label={ENTRY_PROPERTY_TITLES.field}>
<EuiSuperSelect
options={fieldOptions}
@ -147,17 +168,36 @@ export const ConditionEntryInput = memo<ConditionEntryInputProps>(
data-test-subj={getTestId('field')}
/>
</ConditionEntryCell>
</EuiFlexItem>
<EuiFlexItem>
</InputItem>
<InputItem gridArea="operator">
<ConditionEntryCell showLabel={showLabels} label={ENTRY_PROPERTY_TITLES.operator}>
<EuiFieldText name="operator" value={OPERATOR_TITLE.included} readOnly />
{entry.field === ConditionEntryField.PATH ? (
<EuiSuperSelect
options={operatorOptions}
onChange={handleOperatorUpdate}
valueOfSelected={entry.type}
data-test-subj={getTestId('operator')}
/>
) : (
<EuiFieldText
name="operator"
value={OPERATOR_TITLES.is}
data-test-subj={getTestId('operator')}
readOnly
/>
)}
</ConditionEntryCell>
</EuiFlexItem>
<EuiFlexItem grow={3}>
</InputItem>
<InputItem gridArea="value">
<ConditionEntryCell showLabel={showLabels} label={ENTRY_PROPERTY_TITLES.value}>
<EuiFieldText
name="value"
value={entry.value}
placeholder={getPlaceholderTextByOSType({
os,
field: entry.field,
type: entry.type,
})}
fullWidth
required
onChange={handleValueUpdate}
@ -165,8 +205,8 @@ export const ConditionEntryInput = memo<ConditionEntryInputProps>(
data-test-subj={getTestId('value')}
/>
</ConditionEntryCell>
</EuiFlexItem>
<EuiFlexItem grow={false}>
</InputItem>
<InputItem gridArea="remove">
{/* Unicode `nbsp` is used below so that Remove button is property displayed */}
<ConditionEntryCell showLabel={showLabels} label={'\u00A0'}>
<EuiButtonIcon
@ -181,8 +221,8 @@ export const ConditionEntryInput = memo<ConditionEntryInputProps>(
data-test-subj={getTestId('remove')}
/>
</ConditionEntryCell>
</EuiFlexItem>
</EuiFlexGroup>
</InputItem>
</InputGroup>
);
}
);

View file

@ -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<EuiTableFieldDataColumnType<Entry>
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<EuiTableFieldDataColumnType<Entry>
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;
},
},
{

View file

@ -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<

View file

@ -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;
}
}
}

View file

@ -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'),
],
};

View file

@ -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',
};

View file

@ -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<K> };
@ -46,6 +48,7 @@ const OPERATING_SYSTEM_TO_OS_TYPE: Mapping<OperatingSystem, OsType> = {
};
const POLICY_REFERENCE_PREFIX = 'policy:';
const OPERATOR_VALUE = 'included';
const filterUndefined = <T>(list: Array<T | undefined>): T[] => {
return list.filter((item: T | undefined): item is T => item !== undefined);
@ -53,9 +56,10 @@ const filterUndefined = <T>(list: Array<T | undefined>): T[] => {
export const createConditionEntry = <T extends ConditionEntryField>(
field: T,
type: TrustedAppEntryTypes,
value: string
): ConditionEntry<T> => {
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);
}

View file

@ -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'
),
],
});

View file

@ -30,6 +30,24 @@ export const translatedEntryMatchMatcher = t.keyof({
});
export type TranslatedEntryMatchMatcher = t.TypeOf<typeof translatedEntryMatchMatcher>;
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<typeof translatedEntryMatchWildcard>;
export const translatedEntryMatch = t.exact(
t.type({
field: t.string,
@ -61,6 +79,7 @@ export type TranslatedEntryNested = t.TypeOf<typeof translatedEntryNested>;
export const translatedEntry = t.union([
translatedEntryNested,
translatedEntryMatch,
translatedEntryMatchWildcard,
translatedEntryMatchAny,
]);
export type TranslatedEntry = t.TypeOf<typeof translatedEntry>;

View file

@ -20298,7 +20298,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つ以上のフィールド定義が必要です",

View file

@ -20622,7 +20622,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": "至少需要一个字段定义",