Trusted Apps signer API. (#83661)

* Separated out service layer for trusted apps.

* Improved the type structure a bit to avoid using explicit string literals and to add possibility to return OS specific parts of trusted app object in type safe manner.

* Added support for mapping of trusted app to exception item and back.

* Changed schema to support signer in the API.

* Renamed utils to mapping.

* Exported some types in lists plugin and used them in trusted apps.

* Added tests for mapping.

* Added tests for service.

* Switched deletion to use exceptions for not found case.

* Added resetting of the mocks in service layer tests.

* Added handlers tests.

* Refactored mapping tests to be more granular based on the case.

* Restored lowercasing of hash.

* Added schema tests for signer field.

* Removed the grouped tests (they were split into tests for separate concerns).

* Corrected the tests.

* Lowercased the hashes in the service test.

* Moved the lowercasing to the right location.

* Fixed the tests.

* Added test for lowercasing hash value.

* Introduced OperatingSystem enum instead of current types.

* Removed os list constant in favour of separate lists in places that use it (each place has own needs to the ordering).

* Fixed the missed OperatingSystem enum usage.
This commit is contained in:
Bohdan Tsymbala 2020-11-30 15:42:31 +01:00 committed by GitHub
parent b99abe301a
commit de5edaa278
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1435 additions and 1042 deletions

View file

@ -23,6 +23,7 @@ export {
EntryList,
EntriesArray,
NamespaceType,
NestedEntriesArray,
Operator,
OperatorEnum,
OperatorTypeEnum,

View file

@ -11,6 +11,7 @@ import { ListPlugin } from './plugin';
// exporting these since its required at top level in siem plugin
export { ListClient } from './services/lists/list_client';
export { CreateExceptionListItemOptions } from './services/exception_lists/exception_list_client_types';
export { ExceptionListClient } from './services/exception_lists/exception_list_client';
export { ListPluginSetup } from './types';

View file

@ -14,7 +14,6 @@ export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*';
export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurrency';
export const LIMITED_CONCURRENCY_ENDPOINT_COUNT = 100;
export const TRUSTED_APPS_SUPPORTED_OS_TYPES: readonly string[] = ['macos', 'windows', 'linux'];
export const TRUSTED_APPS_LIST_API = '/api/endpoint/trusted_apps';
export const TRUSTED_APPS_CREATE_API = '/api/endpoint/trusted_apps';
export const TRUSTED_APPS_DELETE_API = '/api/endpoint/trusted_apps/{id}';

View file

@ -5,6 +5,7 @@
*/
import { GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema } from './trusted_apps';
import { ConditionEntryField, OperatingSystem } from '../types';
describe('When invoking Trusted Apps Schema', () => {
describe('for GET List', () => {
@ -70,93 +71,62 @@ describe('When invoking Trusted Apps Schema', () => {
});
describe('for POST Create', () => {
const getCreateTrustedAppItem = () => ({
const createConditionEntry = <T>(data?: T) => ({
field: ConditionEntryField.PATH,
type: 'match',
operator: 'included',
value: 'c:/programs files/Anti-Virus',
...(data || {}),
});
const createNewTrustedApp = <T>(data?: T) => ({
name: 'Some Anti-Virus App',
description: 'this one is ok',
os: 'windows',
entries: [
{
field: 'process.executable.caseless',
type: 'match',
operator: 'included',
value: 'c:/programs files/Anti-Virus',
},
],
entries: [createConditionEntry()],
...(data || {}),
});
const body = PostTrustedAppCreateRequestSchema.body;
it('should not error on a valid message', () => {
const bodyMsg = getCreateTrustedAppItem();
const bodyMsg = createNewTrustedApp();
expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg);
});
it('should validate `name` is required', () => {
const bodyMsg = {
...getCreateTrustedAppItem(),
name: undefined,
};
expect(() => body.validate(bodyMsg)).toThrow();
expect(() => body.validate(createNewTrustedApp({ name: undefined }))).toThrow();
});
it('should validate `name` value to be non-empty', () => {
const bodyMsg = {
...getCreateTrustedAppItem(),
name: '',
};
expect(() => body.validate(bodyMsg)).toThrow();
expect(() => body.validate(createNewTrustedApp({ name: '' }))).toThrow();
});
it('should validate `description` as optional', () => {
const { description, ...bodyMsg } = getCreateTrustedAppItem();
const { description, ...bodyMsg } = createNewTrustedApp();
expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg);
});
it('should validate `os` to to only accept known values', () => {
const bodyMsg = {
...getCreateTrustedAppItem(),
os: undefined,
};
const bodyMsg = createNewTrustedApp({ os: undefined });
expect(() => body.validate(bodyMsg)).toThrow();
const bodyMsg2 = {
...bodyMsg,
os: '',
};
expect(() => body.validate(bodyMsg2)).toThrow();
expect(() => body.validate({ ...bodyMsg, os: '' })).toThrow();
const bodyMsg3 = {
...bodyMsg,
os: 'winz',
};
expect(() => body.validate(bodyMsg3)).toThrow();
expect(() => body.validate({ ...bodyMsg, os: 'winz' })).toThrow();
['linux', 'macos', 'windows'].forEach((os) => {
expect(() => {
body.validate({
...bodyMsg,
os,
});
}).not.toThrow();
[OperatingSystem.LINUX, OperatingSystem.MAC, OperatingSystem.WINDOWS].forEach((os) => {
expect(() => body.validate({ ...bodyMsg, os })).not.toThrow();
});
});
it('should validate `entries` as required', () => {
const bodyMsg = {
...getCreateTrustedAppItem(),
entries: undefined,
};
expect(() => body.validate(bodyMsg)).toThrow();
expect(() => body.validate(createNewTrustedApp({ entries: undefined }))).toThrow();
const { entries, ...bodyMsg2 } = getCreateTrustedAppItem();
const { entries, ...bodyMsg2 } = createNewTrustedApp();
expect(() => body.validate(bodyMsg2)).toThrow();
});
it('should validate `entries` to have at least 1 item', () => {
const bodyMsg = {
...getCreateTrustedAppItem(),
entries: [],
};
expect(() => body.validate(bodyMsg)).toThrow();
expect(() => body.validate(createNewTrustedApp({ entries: [] }))).toThrow();
});
describe('when `entries` are defined', () => {
@ -165,171 +135,163 @@ describe('When invoking Trusted Apps Schema', () => {
const VALID_HASH_SHA1 = 'aedb279e378BED6C2DB3C9DC9e12ba635e0b391c';
const VALID_HASH_SHA256 = 'A4370C0CF81686C0B696FA6261c9d3e0d810ae704ab8301839dffd5d5112f476';
const getTrustedAppItemEntryItem = () => getCreateTrustedAppItem().entries[0];
it('should validate `entry.field` is required', () => {
const { field, ...entry } = getTrustedAppItemEntryItem();
const bodyMsg = {
...getCreateTrustedAppItem(),
entries: [entry],
};
const { field, ...entry } = createConditionEntry();
expect(() => body.validate(createNewTrustedApp({ entries: [entry] }))).toThrow();
});
it('should validate `entry.field` does not accept empty values', () => {
const bodyMsg = createNewTrustedApp({
entries: [createConditionEntry({ field: '' })],
});
expect(() => body.validate(bodyMsg)).toThrow();
});
it('should validate `entry.field` is limited to known values', () => {
const bodyMsg = {
...getCreateTrustedAppItem(),
entries: [
{
...getTrustedAppItemEntryItem(),
field: '',
},
],
};
it('should validate `entry.field` does not accept unknown values', () => {
const bodyMsg = createNewTrustedApp({
entries: [createConditionEntry({ field: 'invalid value' })],
});
expect(() => body.validate(bodyMsg)).toThrow();
});
const bodyMsg2 = {
...getCreateTrustedAppItem(),
entries: [
{
...getTrustedAppItemEntryItem(),
field: 'invalid value',
},
],
};
expect(() => body.validate(bodyMsg2)).toThrow();
[
{
field: 'process.hash.*',
value: 'A4370C0CF81686C0B696FA6261c9d3e0d810ae704ab8301839dffd5d5112f476',
},
{ field: 'process.executable.caseless', value: '/tmp/dir1' },
].forEach((partialEntry) => {
const bodyMsg3 = {
...getCreateTrustedAppItem(),
it('should validate `entry.field` accepts hash field name for all os values', () => {
[OperatingSystem.LINUX, OperatingSystem.MAC, OperatingSystem.WINDOWS].forEach((os) => {
const bodyMsg3 = createNewTrustedApp({
os,
entries: [
{
...getTrustedAppItemEntryItem(),
...partialEntry,
},
createConditionEntry({
field: ConditionEntryField.HASH,
value: 'A4370C0CF81686C0B696FA6261c9d3e0d810ae704ab8301839dffd5d5112f476',
}),
],
};
});
expect(() => body.validate(bodyMsg3)).not.toThrow();
});
});
it('should validate `entry.type` is limited to known values', () => {
const bodyMsg = {
...getCreateTrustedAppItem(),
entries: [
{
...getTrustedAppItemEntryItem(),
type: 'invalid',
},
],
};
expect(() => body.validate(bodyMsg)).toThrow();
it('should validate `entry.field` accepts path field name for all os values', () => {
[OperatingSystem.LINUX, OperatingSystem.MAC, OperatingSystem.WINDOWS].forEach((os) => {
const bodyMsg3 = createNewTrustedApp({
os,
entries: [
createConditionEntry({ field: ConditionEntryField.PATH, value: '/tmp/dir1' }),
],
});
// Allow `match`
const bodyMsg2 = {
...getCreateTrustedAppItem(),
entries: [
{
...getTrustedAppItemEntryItem(),
type: 'match',
},
],
};
expect(() => body.validate(bodyMsg2)).not.toThrow();
expect(() => body.validate(bodyMsg3)).not.toThrow();
});
});
it('should validate `entry.operator` is limited to known values', () => {
const bodyMsg = {
...getCreateTrustedAppItem(),
it('should validate `entry.field` accepts signer field name for windows os value', () => {
const bodyMsg3 = createNewTrustedApp({
os: 'windows',
entries: [
{
...getTrustedAppItemEntryItem(),
operator: 'invalid',
},
createConditionEntry({ field: ConditionEntryField.SIGNER, value: 'Microsoft' }),
],
};
expect(() => body.validate(bodyMsg)).toThrow();
});
// Allow `match`
const bodyMsg2 = {
...getCreateTrustedAppItem(),
entries: [
{
...getTrustedAppItemEntryItem(),
operator: 'included',
},
],
};
expect(() => body.validate(bodyMsg2)).not.toThrow();
expect(() => body.validate(bodyMsg3)).not.toThrow();
});
it('should validate `entry.field` does not accept signer field name for linux and macos os values', () => {
[OperatingSystem.LINUX, OperatingSystem.MAC].forEach((os) => {
const bodyMsg3 = createNewTrustedApp({
os,
entries: [
createConditionEntry({ field: ConditionEntryField.SIGNER, value: 'Microsoft' }),
],
});
expect(() => body.validate(bodyMsg3)).toThrow();
});
});
it('should validate `entry.type` does not accept unknown values', () => {
const bodyMsg = createNewTrustedApp({
entries: [createConditionEntry({ type: 'invalid' })],
});
expect(() => body.validate(bodyMsg)).toThrow();
});
it('should validate `entry.type` accepts known values', () => {
const bodyMsg = createNewTrustedApp({
entries: [createConditionEntry({ type: 'match' })],
});
expect(() => body.validate(bodyMsg)).not.toThrow();
});
it('should validate `entry.operator` does not accept unknown values', () => {
const bodyMsg = createNewTrustedApp({
entries: [createConditionEntry({ operator: 'invalid' })],
});
expect(() => body.validate(bodyMsg)).toThrow();
});
it('should validate `entry.operator` accepts known values', () => {
const bodyMsg = createNewTrustedApp({
entries: [createConditionEntry({ operator: 'included' })],
});
expect(() => body.validate(bodyMsg)).not.toThrow();
});
it('should validate `entry.value` required', () => {
const { value, ...entry } = getTrustedAppItemEntryItem();
const bodyMsg = {
...getCreateTrustedAppItem(),
entries: [entry],
};
expect(() => body.validate(bodyMsg)).toThrow();
const { value, ...entry } = createConditionEntry();
expect(() => body.validate(createNewTrustedApp({ entries: [entry] }))).toThrow();
});
it('should validate `entry.value` is non-empty', () => {
const bodyMsg = {
...getCreateTrustedAppItem(),
entries: [
{
...getTrustedAppItemEntryItem(),
value: '',
},
],
};
const bodyMsg = createNewTrustedApp({ entries: [createConditionEntry({ value: '' })] });
expect(() => body.validate(bodyMsg)).toThrow();
});
it('should validate that `entry.field` is used only once', () => {
let bodyMsg = {
...getCreateTrustedAppItem(),
entries: [getTrustedAppItemEntryItem(), getTrustedAppItemEntryItem()],
};
it('should validate that `entry.field` path field value can only be used once', () => {
const bodyMsg = createNewTrustedApp({
entries: [createConditionEntry(), createConditionEntry()],
});
expect(() => body.validate(bodyMsg)).toThrow('[Path] field can only be used once');
});
bodyMsg = {
...getCreateTrustedAppItem(),
it('should validate that `entry.field` hash field value can only be used once', () => {
const bodyMsg = createNewTrustedApp({
entries: [
{
...getTrustedAppItemEntryItem(),
field: 'process.hash.*',
createConditionEntry({
field: ConditionEntryField.HASH,
value: VALID_HASH_MD5,
},
{
...getTrustedAppItemEntryItem(),
field: 'process.hash.*',
}),
createConditionEntry({
field: ConditionEntryField.HASH,
value: VALID_HASH_MD5,
},
}),
],
};
});
expect(() => body.validate(bodyMsg)).toThrow('[Hash] field can only be used once');
});
it('should validate that `entry.field` signer field value can only be used once', () => {
const bodyMsg = createNewTrustedApp({
entries: [
createConditionEntry({
field: ConditionEntryField.SIGNER,
value: 'Microsoft',
}),
createConditionEntry({
field: ConditionEntryField.SIGNER,
value: 'Microsoft',
}),
],
});
expect(() => body.validate(bodyMsg)).toThrow('[Signer] field can only be used once');
});
it('should validate Hash field valid value', () => {
[VALID_HASH_MD5, VALID_HASH_SHA1, VALID_HASH_SHA256].forEach((value) => {
expect(() => {
body.validate({
...getCreateTrustedAppItem(),
entries: [
{
...getTrustedAppItemEntryItem(),
field: 'process.hash.*',
value,
},
],
});
body.validate(
createNewTrustedApp({
entries: [createConditionEntry({ field: ConditionEntryField.HASH, value })],
})
);
}).not.toThrow();
});
});
@ -337,49 +299,29 @@ describe('When invoking Trusted Apps Schema', () => {
it('should validate Hash value with invalid length', () => {
['xyz', VALID_HASH_SHA256 + VALID_HASH_MD5].forEach((value) => {
expect(() => {
body.validate({
...getCreateTrustedAppItem(),
entries: [
{
...getTrustedAppItemEntryItem(),
field: 'process.hash.*',
value,
},
],
});
body.validate(
createNewTrustedApp({
entries: [createConditionEntry({ field: ConditionEntryField.HASH, value })],
})
);
}).toThrow();
});
});
it('should validate Hash value with invalid characters', () => {
expect(() => {
body.validate({
...getCreateTrustedAppItem(),
entries: [
{
...getTrustedAppItemEntryItem(),
field: 'process.hash.*',
value: `G${VALID_HASH_MD5.substr(1)}`,
},
],
});
body.validate(
createNewTrustedApp({
entries: [
createConditionEntry({
field: ConditionEntryField.HASH,
value: `G${VALID_HASH_MD5.substr(1)}`,
}),
],
})
);
}).toThrow();
});
it('should trim hash value before validation', () => {
expect(() => {
body.validate({
...getCreateTrustedAppItem(),
entries: [
{
...getTrustedAppItemEntryItem(),
field: 'process.hash.*',
value: ` ${VALID_HASH_MD5} \r\n`,
},
],
});
}).not.toThrow();
});
});
});
});

View file

@ -4,22 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { TrustedApp } from '../types';
import { schema, Type } from '@kbn/config-schema';
import { ConditionEntry, ConditionEntryField, OperatingSystem } from '../types';
const hashLengths: readonly number[] = [
const HASH_LENGTHS: readonly number[] = [
32, // MD5
40, // SHA1
64, // SHA256
];
const hasInvalidCharacters = /[^0-9a-f]/i;
const INVALID_CHARACTERS_PATTERN = /[^0-9a-f]/i;
const entryFieldLabels: { [k in TrustedApp['entries'][0]['field']]: string } = {
'process.hash.*': 'Hash',
'process.executable.caseless': 'Path',
'process.code_signature': 'Signer',
const entryFieldLabels: { [k in ConditionEntryField]: string } = {
[ConditionEntryField.HASH]: 'Hash',
[ConditionEntryField.PATH]: 'Path',
[ConditionEntryField.SIGNER]: 'Signer',
};
const isValidHash = (value: string) =>
HASH_LENGTHS.includes(value.length) && !INVALID_CHARACTERS_PATTERN.test(value);
export const DeleteTrustedAppsRequestSchema = {
params: schema.object({
id: schema.string(),
@ -33,17 +36,17 @@ export const GetTrustedAppsRequestSchema = {
}),
};
export const PostTrustedAppCreateRequestSchema = {
body: schema.object({
const createNewTrustedAppForOsScheme = <O extends OperatingSystem, F extends ConditionEntryField>(
osSchema: Type<O>,
fieldSchema: Type<F>
) =>
schema.object({
name: schema.string({ minLength: 1, maxLength: 256 }),
description: schema.maybe(schema.string({ minLength: 0, maxLength: 256, defaultValue: '' })),
os: schema.oneOf([schema.literal('linux'), schema.literal('macos'), schema.literal('windows')]),
os: osSchema,
entries: schema.arrayOf(
schema.object({
field: schema.oneOf([
schema.literal('process.hash.*'),
schema.literal('process.executable.caseless'),
]),
field: fieldSchema,
type: schema.literal('match'),
operator: schema.literal('included'),
value: schema.string({ minLength: 1 }),
@ -51,27 +54,43 @@ export const PostTrustedAppCreateRequestSchema = {
{
minSize: 1,
validate(entries) {
const usedFields: string[] = [];
for (const { field, value } of entries) {
if (usedFields.includes(field)) {
const usedFields = new Set();
for (const entry of entries) {
// unfortunately combination of generics and Type<...> for "field" causes type errors
const { field, value } = entry as ConditionEntry<ConditionEntryField>;
if (usedFields.has(field)) {
return `[${entryFieldLabels[field]}] field can only be used once`;
}
usedFields.push(field);
usedFields.add(field);
if (field === 'process.hash.*') {
const trimmedValue = value.trim();
if (
!hashLengths.includes(trimmedValue.length) ||
hasInvalidCharacters.test(trimmedValue)
) {
return `Invalid hash value [${value}]`;
}
if (field === ConditionEntryField.HASH && !isValidHash(value)) {
return `Invalid hash value [${value}]`;
}
}
},
}
),
}),
});
export const PostTrustedAppCreateRequestSchema = {
body: schema.oneOf([
createNewTrustedAppForOsScheme(
schema.oneOf([schema.literal(OperatingSystem.LINUX), schema.literal(OperatingSystem.MAC)]),
schema.oneOf([
schema.literal(ConditionEntryField.HASH),
schema.literal(ConditionEntryField.PATH),
])
),
createNewTrustedAppForOsScheme(
schema.literal(OperatingSystem.WINDOWS),
schema.oneOf([
schema.literal(ConditionEntryField.HASH),
schema.literal(ConditionEntryField.PATH),
schema.literal(ConditionEntryField.SIGNER),
])
),
]),
};

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
export type Linux = 'linux';
export type MacOS = 'macos';
export type Windows = 'windows';
export type OperatingSystem = Linux | MacOS | Windows;
export enum OperatingSystem {
LINUX = 'linux',
MAC = 'macos',
WINDOWS = 'windows',
}

View file

@ -11,7 +11,7 @@ import {
GetTrustedAppsRequestSchema,
PostTrustedAppCreateRequestSchema,
} from '../schema/trusted_apps';
import { Linux, MacOS, Windows } from './os';
import { OperatingSystem } from './os';
/** API request params for deleting Trusted App entry */
export type DeleteTrustedAppsRequestParams = TypeOf<typeof DeleteTrustedAppsRequestSchema.params>;
@ -33,33 +33,41 @@ export interface PostTrustedAppCreateResponse {
data: TrustedApp;
}
export interface MacosLinuxConditionEntry {
field: 'process.hash.*' | 'process.executable.caseless';
export enum ConditionEntryField {
HASH = 'process.hash.*',
PATH = 'process.executable.caseless',
SIGNER = 'process.Ext.code_signature',
}
export interface ConditionEntry<T extends ConditionEntryField> {
field: T;
type: 'match';
operator: 'included';
value: string;
}
export type WindowsConditionEntry =
| MacosLinuxConditionEntry
| (Omit<MacosLinuxConditionEntry, 'field'> & {
field: 'process.code_signature';
});
export type MacosLinuxConditionEntry = ConditionEntry<
ConditionEntryField.HASH | ConditionEntryField.PATH
>;
export type WindowsConditionEntry = ConditionEntry<
ConditionEntryField.HASH | ConditionEntryField.PATH | ConditionEntryField.SIGNER
>;
export interface MacosLinuxConditionEntries {
os: OperatingSystem.LINUX | OperatingSystem.MAC;
entries: MacosLinuxConditionEntry[];
}
export interface WindowsConditionEntries {
os: OperatingSystem.WINDOWS;
entries: WindowsConditionEntry[];
}
/** Type for a new Trusted App Entry */
export type NewTrustedApp = {
name: string;
description?: string;
} & (
| {
os: Linux | MacOS;
entries: MacosLinuxConditionEntry[];
}
| {
os: Windows;
entries: WindowsConditionEntry[];
}
);
} & (MacosLinuxConditionEntries | WindowsConditionEntries);
/** A trusted app entry */
export type TrustedApp = NewTrustedApp & {

View file

@ -25,13 +25,13 @@ export const BETA_BADGE_LABEL = i18n.translate('xpack.securitySolution.administr
});
export const OS_TITLES: Readonly<{ [K in OperatingSystem]: string }> = {
windows: i18n.translate('xpack.securitySolution.administration.os.windows', {
[OperatingSystem.WINDOWS]: i18n.translate('xpack.securitySolution.administration.os.windows', {
defaultMessage: 'Windows',
}),
macos: i18n.translate('xpack.securitySolution.administration.os.macos', {
[OperatingSystem.MAC]: i18n.translate('xpack.securitySolution.administration.os.macos', {
defaultMessage: 'Mac',
}),
linux: i18n.translate('xpack.securitySolution.administration.os.linux', {
[OperatingSystem.LINUX]: i18n.translate('xpack.securitySolution.administration.os.linux', {
defaultMessage: 'Linux',
}),
};

View file

@ -5,10 +5,12 @@
*/
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { storiesOf, addDecorator } from '@storybook/react';
import { addDecorator, storiesOf } from '@storybook/react';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { EuiCheckbox, EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui';
import { OperatingSystem } from '../../../../../../../common/endpoint/types';
import { ConfigForm } from '.';
addDecorator((storyFn) => (
@ -18,21 +20,24 @@ addDecorator((storyFn) => (
storiesOf('PolicyDetails/ConfigForm', module)
.add('One OS', () => {
return (
<ConfigForm type="Type 1" supportedOss={['windows']}>
<ConfigForm type="Type 1" supportedOss={[OperatingSystem.WINDOWS]}>
{'Some content'}
</ConfigForm>
);
})
.add('Multiple OSs', () => {
return (
<ConfigForm type="Type 1" supportedOss={['windows', 'macos', 'linux']}>
<ConfigForm
type="Type 1"
supportedOss={[OperatingSystem.WINDOWS, OperatingSystem.MAC, OperatingSystem.LINUX]}
>
{'Some content'}
</ConfigForm>
);
})
.add('Complex content', () => {
return (
<ConfigForm type="Type 1" supportedOss={['macos', 'linux']}>
<ConfigForm type="Type 1" supportedOss={[OperatingSystem.MAC, OperatingSystem.LINUX]}>
<EuiText>
{'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore ' +
'et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut ' +
@ -53,7 +58,7 @@ storiesOf('PolicyDetails/ConfigForm', module)
const toggle = <EuiSwitch label={'Switch'} checked={true} onChange={() => {}} />;
return (
<ConfigForm type="Type 1" supportedOss={['linux']} rightCorner={toggle}>
<ConfigForm type="Type 1" supportedOss={[OperatingSystem.LINUX]} rightCorner={toggle}>
{'Some content'}
</ConfigForm>
);

View file

@ -9,6 +9,7 @@ import { useDispatch } from 'react-redux';
import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui';
import { OperatingSystem } from '../../../../../../../common/endpoint/types';
import { isAntivirusRegistrationEnabled } from '../../../store/policy_details/selectors';
import { usePolicyDetailsSelector } from '../../policy_hooks';
import { ConfigForm } from '../../components/config_form';
@ -36,7 +37,7 @@ export const AntivirusRegistrationForm = memo(() => {
defaultMessage: 'Register as anti-virus',
}
)}
supportedOss={['windows']}
supportedOss={[OperatingSystem.WINDOWS]}
>
<EuiText size="s">
{i18n.translate(

View file

@ -6,14 +6,14 @@
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiText, EuiSpacer } from '@elastic/eui';
import { EuiSpacer, EuiText } from '@elastic/eui';
import { EventsCheckbox } from './checkbox';
import { OS } from '../../../types';
import { usePolicyDetailsSelector } from '../../policy_hooks';
import { selectedLinuxEvents, totalLinuxEvents } from '../../../store/policy_details/selectors';
import { ConfigForm, ConfigFormHeading } from '../../components/config_form';
import { getIn, setIn } from '../../../models/policy_details_config';
import { UIPolicyConfig } from '../../../../../../../common/endpoint/types';
import { OperatingSystem, UIPolicyConfig } from '../../../../../../../common/endpoint/types';
import {
COLLECTIONS_ENABLED_MESSAGE,
EVENTS_FORM_TYPE_LABEL,
@ -85,7 +85,7 @@ export const LinuxEvents = React.memo(() => {
return (
<ConfigForm
type={EVENTS_FORM_TYPE_LABEL}
supportedOss={['linux']}
supportedOss={[OperatingSystem.LINUX]}
dataTestSubj="linuxEventingForm"
rightCorner={
<EuiText size="s" color="subdued">

View file

@ -6,14 +6,14 @@
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiText, EuiSpacer } from '@elastic/eui';
import { EuiSpacer, EuiText } from '@elastic/eui';
import { EventsCheckbox } from './checkbox';
import { OS } from '../../../types';
import { usePolicyDetailsSelector } from '../../policy_hooks';
import { selectedMacEvents, totalMacEvents } from '../../../store/policy_details/selectors';
import { ConfigForm, ConfigFormHeading } from '../../components/config_form';
import { getIn, setIn } from '../../../models/policy_details_config';
import { UIPolicyConfig } from '../../../../../../../common/endpoint/types';
import { OperatingSystem, UIPolicyConfig } from '../../../../../../../common/endpoint/types';
import {
COLLECTIONS_ENABLED_MESSAGE,
EVENTS_FORM_TYPE_LABEL,
@ -85,7 +85,7 @@ export const MacEvents = React.memo(() => {
return (
<ConfigForm
type={EVENTS_FORM_TYPE_LABEL}
supportedOss={['macos']}
supportedOss={[OperatingSystem.MAC]}
dataTestSubj="macEventingForm"
rightCorner={
<EuiText size="s" color="subdued">

View file

@ -6,14 +6,18 @@
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiText, EuiSpacer } from '@elastic/eui';
import { EuiSpacer, EuiText } from '@elastic/eui';
import { EventsCheckbox } from './checkbox';
import { OS } from '../../../types';
import { usePolicyDetailsSelector } from '../../policy_hooks';
import { selectedWindowsEvents, totalWindowsEvents } from '../../../store/policy_details/selectors';
import { ConfigForm, ConfigFormHeading } from '../../components/config_form';
import { setIn, getIn } from '../../../models/policy_details_config';
import { UIPolicyConfig, Immutable } from '../../../../../../../common/endpoint/types';
import { getIn, setIn } from '../../../models/policy_details_config';
import {
Immutable,
OperatingSystem,
UIPolicyConfig,
} from '../../../../../../../common/endpoint/types';
import {
COLLECTIONS_ENABLED_MESSAGE,
EVENTS_FORM_TYPE_LABEL,
@ -127,7 +131,7 @@ export const WindowsEvents = React.memo(() => {
return (
<ConfigForm
type={EVENTS_FORM_TYPE_LABEL}
supportedOss={['windows']}
supportedOss={[OperatingSystem.WINDOWS]}
dataTestSubj="windowsEventingForm"
rightCorner={
<EuiText size="s" color="subdued">

View file

@ -10,21 +10,25 @@ import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiRadio,
EuiSwitch,
EuiText,
EuiSpacer,
EuiTextArea,
htmlIdGenerator,
EuiCallOut,
EuiCheckbox,
EuiRadio,
EuiSpacer,
EuiSwitch,
EuiText,
EuiTextArea,
htmlIdGenerator,
} from '@elastic/eui';
import { cloneDeep } from 'lodash';
import { APP_ID } from '../../../../../../../common/constants';
import { SecurityPageName } from '../../../../../../app/types';
import { Immutable, ProtectionModes } from '../../../../../../../common/endpoint/types';
import { OS, MalwareProtectionOSes } from '../../../types';
import {
Immutable,
OperatingSystem,
ProtectionModes,
} from '../../../../../../../common/endpoint/types';
import { MalwareProtectionOSes, OS } from '../../../types';
import { ConfigForm, ConfigFormHeading } from '../../components/config_form';
import { policyConfig } from '../../../store/policy_details/selectors';
import { usePolicyDetailsSelector } from '../../policy_hooks';
@ -305,7 +309,7 @@ export const MalwareProtections = React.memo(() => {
type={i18n.translate('xpack.securitySolution.endpoint.policy.details.malware', {
defaultMessage: 'Malware',
})}
supportedOss={['windows', 'macos']}
supportedOss={[OperatingSystem.WINDOWS, OperatingSystem.MAC]}
dataTestSubj="malwareProtectionsForm"
rightCorner={protectionSwitch}
>

View file

@ -11,12 +11,12 @@ import {
TrustedAppCreateSuccess,
} from './trusted_apps_list_page_state';
import {
ConditionEntry,
ConditionEntryField,
Immutable,
MacosLinuxConditionEntry,
NewTrustedApp,
WindowsConditionEntry,
} from '../../../../../common/endpoint/types';
import { TRUSTED_APPS_SUPPORTED_OS_TYPES } from '../../../../../common/endpoint/constants';
type CreateViewPossibleStates =
| TrustedAppsListPageState['createView']
@ -40,23 +40,14 @@ export const isTrustedAppCreateFailureState = (
return data?.type === 'failure';
};
export const isWindowsTrustedApp = <T extends NewTrustedApp = NewTrustedApp>(
trustedApp: T
): trustedApp is T & { os: 'windows' } => {
return trustedApp.os === 'windows';
export const isWindowsTrustedAppCondition = (
condition: ConditionEntry<ConditionEntryField>
): condition is WindowsConditionEntry => {
return condition.field === ConditionEntryField.SIGNER || true;
};
export const isWindowsTrustedAppCondition = (condition: {
field: string;
}): condition is WindowsConditionEntry => {
return condition.field === 'process.code_signature' || true;
export const isMacosLinuxTrustedAppCondition = (
condition: ConditionEntry<ConditionEntryField>
): condition is MacosLinuxConditionEntry => {
return condition.field !== ConditionEntryField.SIGNER;
};
export const isMacosLinuxTrustedAppCondition = (condition: {
field: string;
}): condition is MacosLinuxConditionEntry => {
return condition.field !== 'process.code_signature' || true;
};
export const isTrustedAppSupportedOs = (os: string): os is NewTrustedApp['os'] =>
TRUSTED_APPS_SUPPORTED_OS_TYPES.includes(os);

View file

@ -5,7 +5,7 @@
*/
import { combineReducers, createStore } from 'redux';
import { TrustedApp } from '../../../../../common/endpoint/types';
import { TrustedApp, OperatingSystem } from '../../../../../common/endpoint/types';
import { RoutingAction } from '../../../../common/store/routing';
import {
@ -31,7 +31,11 @@ import {
import { trustedAppsPageReducer } from '../store/reducer';
import { TrustedAppsListResourceStateChanged } from '../store/action';
const OS_LIST: Array<TrustedApp['os']> = ['windows', 'macos', 'linux'];
const OPERATING_SYSTEMS: OperatingSystem[] = [
OperatingSystem.WINDOWS,
OperatingSystem.MAC,
OperatingSystem.LINUX,
];
const generate = <T>(count: number, generator: (i: number) => T) =>
[...new Array(count).keys()].map(generator);
@ -43,7 +47,7 @@ export const createSampleTrustedApp = (i: number, longTexts?: boolean): TrustedA
description: generate(longTexts ? 10 : 1, () => `Trusted App ${i}`).join(' '),
created_at: '1 minute ago',
created_by: 'someone',
os: OS_LIST[i % 3],
os: OPERATING_SYSTEMS[i % 3],
entries: [],
};
};

View file

@ -4,14 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import * as reactTestingLibrary from '@testing-library/react';
import { fireEvent, getByTestId } from '@testing-library/dom';
import { ConditionEntryField, OperatingSystem } from '../../../../../../common/endpoint/types';
import {
AppContextTestRender,
createAppRootMockRenderer,
} from '../../../../../common/mock/endpoint';
import * as reactTestingLibrary from '@testing-library/react';
import React from 'react';
import { CreateTrustedAppForm, CreateTrustedAppFormProps } from './create_trusted_app_form';
import { fireEvent, getByTestId } from '@testing-library/dom';
describe('When showing the Trusted App Create Form', () => {
const dataTestSubjForForm = 'createForm';
@ -234,14 +237,14 @@ describe('When showing the Trusted App Create Form', () => {
description: '',
entries: [
{
field: 'process.hash.*',
field: ConditionEntryField.HASH,
operator: 'included',
type: 'match',
value: '',
},
],
name: '',
os: 'windows',
os: OperatingSystem.WINDOWS,
},
});
});
@ -289,14 +292,14 @@ describe('When showing the Trusted App Create Form', () => {
description: 'some description',
entries: [
{
field: 'process.hash.*',
field: ConditionEntryField.HASH,
operator: 'included',
type: 'match',
value: 'someHASH',
},
],
name: 'Some Process',
os: 'windows',
os: OperatingSystem.WINDOWS,
},
});
});

View file

@ -6,34 +6,39 @@
import React, { ChangeEventHandler, memo, useCallback, useEffect, useMemo, useState } from 'react';
import {
EuiFieldText,
EuiForm,
EuiFormRow,
EuiFieldText,
EuiSuperSelect,
EuiSuperSelectOption,
EuiTextArea,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiFormProps } from '@elastic/eui/src/components/form/form';
import { TRUSTED_APPS_SUPPORTED_OS_TYPES } from '../../../../../../common/endpoint/constants';
import { LogicalConditionBuilder } from './logical_condition';
import {
ConditionEntry,
ConditionEntryField,
MacosLinuxConditionEntry,
NewTrustedApp,
TrustedApp,
OperatingSystem,
} from '../../../../../../common/endpoint/types';
import { LogicalConditionBuilderProps } from './logical_condition/logical_condition_builder';
import { OS_TITLES } from '../translations';
import {
isMacosLinuxTrustedAppCondition,
isTrustedAppSupportedOs,
isWindowsTrustedApp,
isWindowsTrustedAppCondition,
} from '../../state/type_guards';
const generateNewEntry = (): NewTrustedApp['entries'][0] => {
const OPERATING_SYSTEMS: readonly OperatingSystem[] = [
OperatingSystem.MAC,
OperatingSystem.WINDOWS,
OperatingSystem.LINUX,
];
const generateNewEntry = (): ConditionEntry<ConditionEntryField.HASH> => {
return {
field: 'process.hash.*',
field: ConditionEntryField.HASH,
operator: 'included',
type: 'match',
value: '',
@ -160,18 +165,14 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
({ fullWidth, onChange, ...formProps }) => {
const dataTestSubj = formProps['data-test-subj'];
const osOptions: Array<EuiSuperSelectOption<string>> = useMemo(() => {
return TRUSTED_APPS_SUPPORTED_OS_TYPES.map((os) => {
return {
value: os,
inputDisplay: OS_TITLES[os as TrustedApp['os']],
};
});
}, []);
const osOptions: Array<EuiSuperSelectOption<OperatingSystem>> = useMemo(
() => OPERATING_SYSTEMS.map((os) => ({ value: os, inputDisplay: OS_TITLES[os] })),
[]
);
const [formValues, setFormValues] = useState<NewTrustedApp>({
name: '',
os: 'windows',
os: OperatingSystem.WINDOWS,
entries: [generateNewEntry()],
description: '',
});
@ -200,20 +201,20 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
const handleAndClick = useCallback(() => {
setFormValues(
(prevState): NewTrustedApp => {
if (isWindowsTrustedApp(prevState)) {
if (prevState.os === OperatingSystem.WINDOWS) {
return {
...prevState,
entries: [...prevState.entries, generateNewEntry()].filter((entry) =>
isWindowsTrustedAppCondition(entry)
entries: [...prevState.entries, generateNewEntry()].filter(
isWindowsTrustedAppCondition
),
};
} else {
return {
...prevState,
entries: [
...prevState.entries.filter((entry) => isMacosLinuxTrustedAppCondition(entry)),
...prevState.entries.filter(isMacosLinuxTrustedAppCondition),
generateNewEntry(),
] as MacosLinuxConditionEntry[],
],
};
}
}
@ -245,30 +246,27 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
[]
);
const handleOsChange = useCallback<(v: string) => void>((newOsValue) => {
const handleOsChange = useCallback<(v: OperatingSystem) => void>((newOsValue) => {
setFormValues(
(prevState): NewTrustedApp => {
if (isTrustedAppSupportedOs(newOsValue)) {
const updatedState: NewTrustedApp = {
...prevState,
entries: [],
os: newOsValue,
};
if (!isWindowsTrustedApp(updatedState)) {
updatedState.entries.push(
...(prevState.entries.filter((entry) =>
isMacosLinuxTrustedAppCondition(entry)
) as MacosLinuxConditionEntry[])
);
if (updatedState.entries.length === 0) {
updatedState.entries.push(generateNewEntry() as MacosLinuxConditionEntry);
}
} else {
updatedState.entries.push(...prevState.entries);
const updatedState: NewTrustedApp = {
...prevState,
entries: [],
os: newOsValue,
};
if (updatedState.os !== OperatingSystem.WINDOWS) {
updatedState.entries.push(
...(prevState.entries.filter((entry) =>
isMacosLinuxTrustedAppCondition(entry)
) as MacosLinuxConditionEntry[])
);
if (updatedState.entries.length === 0) {
updatedState.entries.push(generateNewEntry());
}
return updatedState;
} else {
updatedState.entries.push(...prevState.entries);
}
return prevState;
return updatedState;
}
);
setWasVisited((prevState) => {
@ -294,7 +292,7 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
(newEntry, oldEntry) => {
setFormValues(
(prevState): NewTrustedApp => {
if (isWindowsTrustedApp(prevState)) {
if (prevState.os === OperatingSystem.WINDOWS) {
return {
...prevState,
entries: prevState.entries.map((item) => {

View file

@ -5,6 +5,7 @@
*/
import React, { ChangeEventHandler, memo, useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFlexGroup,
EuiFlexItem,
@ -14,8 +15,8 @@ import {
EuiButtonIcon,
EuiSuperSelectOption,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { TrustedApp } from '../../../../../../../../common/endpoint/types';
import { ConditionEntryField, TrustedApp } from '../../../../../../../../common/endpoint/types';
import { CONDITION_FIELD_TITLE } from '../../../translations';
const ConditionEntryCell = memo<{
@ -73,12 +74,12 @@ export const ConditionEntry = memo<ConditionEntryProps>(
const fieldOptions = useMemo<Array<EuiSuperSelectOption<string>>>(() => {
return [
{
inputDisplay: CONDITION_FIELD_TITLE['process.hash.*'],
value: 'process.hash.*',
inputDisplay: CONDITION_FIELD_TITLE[ConditionEntryField.HASH],
value: ConditionEntryField.HASH,
},
{
inputDisplay: CONDITION_FIELD_TITLE['process.executable.caseless'],
value: 'process.executable.caseless',
inputDisplay: CONDITION_FIELD_TITLE[ConditionEntryField.PATH],
value: ConditionEntryField.PATH,
},
];
}, []);

View file

@ -8,7 +8,7 @@ import React, { memo, useCallback } from 'react';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiHideFor, EuiSpacer } from '@elastic/eui';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
import { NewTrustedApp, TrustedApp } from '../../../../../../../../common/endpoint/types';
import { TrustedApp, WindowsConditionEntry } from '../../../../../../../../common/endpoint/types';
import { ConditionEntry, ConditionEntryProps } from './condition_entry';
import { AndOrBadge } from '../../../../../../../common/components/and_or_badge';
@ -85,7 +85,7 @@ export const ConditionGroup = memo<ConditionGroupProps>(
)}
<EuiFlexItem grow={1}>
<div data-test-subj={getTestId('entries')} className="group-entries">
{(entries as (NewTrustedApp & { os: 'windows' })['entries']).map((entry, index) => (
{(entries as WindowsConditionEntry[]).map((entry, index) => (
<ConditionEntry
key={index}
os={os}

View file

@ -10,7 +10,11 @@ import { action } from '@storybook/addon-actions';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { KibanaContextProvider } from '../../../../../../../../../../src/plugins/kibana_react/public';
import { TrustedApp, WindowsConditionEntry } from '../../../../../../../common/endpoint/types';
import {
ConditionEntryField,
TrustedApp,
WindowsConditionEntry,
} from '../../../../../../../common/endpoint/types';
import { createSampleTrustedApp } from '../../../test_utils';
@ -25,14 +29,14 @@ addDecorator((storyFn) => (
));
const PATH_CONDITION: WindowsConditionEntry = {
field: 'process.executable.caseless',
field: ConditionEntryField.PATH,
operator: 'included',
type: 'match',
value: '/some/path/on/file/system',
};
const SIGNER_CONDITION: WindowsConditionEntry = {
field: 'process.code_signature',
field: ConditionEntryField.SIGNER,
operator: 'included',
type: 'match',
value: 'Elastic',

View file

@ -9,33 +9,34 @@ import {
TrustedApp,
MacosLinuxConditionEntry,
WindowsConditionEntry,
ConditionEntry,
ConditionEntryField,
} from '../../../../../common/endpoint/types';
export { OS_TITLES } from '../../../common/translations';
export const ABOUT_TRUSTED_APPS = i18n.translate('xpack.securitySolution.trustedapps.aboutInfo', {
defaultMessage:
'Add a trusted application to improve performance or alleviate conflicts with other applications running on your hosts. Trusted applications will be applied to hosts running Endpoint Security.',
'Add a trusted application to improve performance or alleviate conflicts with other applications ' +
'running on your hosts. Trusted applications will be applied to hosts running Endpoint Security.',
});
type Entry = MacosLinuxConditionEntry | WindowsConditionEntry;
export const CONDITION_FIELD_TITLE: { [K in Entry['field']]: string } = {
'process.hash.*': i18n.translate(
export const CONDITION_FIELD_TITLE: { [K in ConditionEntryField]: string } = {
[ConditionEntryField.HASH]: i18n.translate(
'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.hash',
{ defaultMessage: 'Hash' }
),
'process.executable.caseless': i18n.translate(
[ConditionEntryField.PATH]: i18n.translate(
'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.path',
{ defaultMessage: 'Path' }
),
'process.code_signature': i18n.translate(
[ConditionEntryField.SIGNER]: i18n.translate(
'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.signature',
{ defaultMessage: 'Signature' }
),
};
export const OPERATOR_TITLE: { [K in Entry['operator']]: string } = {
export const OPERATOR_TITLE: { [K in ConditionEntry<ConditionEntryField>['operator']]: string } = {
included: i18n.translate('xpack.securitySolution.trustedapps.card.operator.includes', {
defaultMessage: 'is',
}),

View file

@ -0,0 +1,233 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { KibanaResponseFactory } from 'kibana/server';
import { xpackMocks } from '../../../../../../mocks';
import { loggingSystemMock, httpServerMock } from '../../../../../../../src/core/server/mocks';
import { ExceptionListItemSchema } from '../../../../../lists/common/schemas/response';
import { listMock } from '../../../../../lists/server/mocks';
import { ExceptionListClient } from '../../../../../lists/server';
import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__';
import { ConditionEntryField, OperatingSystem } from '../../../../common/endpoint/types';
import { EndpointAppContextService } from '../../endpoint_app_context_services';
import { createConditionEntry, createEntryMatch } from './mapping';
import {
getTrustedAppsCreateRouteHandler,
getTrustedAppsDeleteRouteHandler,
getTrustedAppsListRouteHandler,
} from './handlers';
const exceptionsListClient = listMock.getExceptionListClient() as jest.Mocked<ExceptionListClient>;
const createAppContextMock = () => ({
logFactory: loggingSystemMock.create(),
service: new EndpointAppContextService(),
config: () => Promise.resolve(createMockConfig()),
});
const createHandlerContextMock = () => ({
...xpackMocks.createRequestHandlerContext(),
lists: {
getListClient: jest.fn(),
getExceptionListClient: jest.fn().mockReturnValue(exceptionsListClient),
},
});
const assertResponse = <T>(
response: jest.Mocked<KibanaResponseFactory>,
expectedResponseType: keyof KibanaResponseFactory,
expectedResponseBody: T
) => {
expect(response[expectedResponseType]).toBeCalled();
expect(response[expectedResponseType].mock.calls[0][0]?.body).toEqual(expectedResponseBody);
};
const EXCEPTION_LIST_ITEM: ExceptionListItemSchema = {
_version: '123',
id: '123',
comments: [],
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
description: 'Linux trusted app 1',
entries: [
createEntryMatch('process.executable.caseless', '/bin/malware'),
createEntryMatch('process.hash.md5', '1234234659af249ddf3e40864e9fb241'),
],
item_id: '123',
list_id: 'endpoint_trusted_apps',
meta: undefined,
name: 'linux trusted app 1',
namespace_type: 'agnostic',
os_types: ['linux'],
tags: [],
type: 'simple',
tie_breaker_id: '123',
updated_at: '11/11/2011T11:11:11.111',
updated_by: 'admin',
};
const NEW_TRUSTED_APP = {
name: 'linux trusted app 1',
description: 'Linux trusted app 1',
os: OperatingSystem.LINUX,
entries: [
createConditionEntry(ConditionEntryField.PATH, '/bin/malware'),
createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'),
],
};
const TRUSTED_APP = {
id: '123',
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
name: 'linux trusted app 1',
description: 'Linux trusted app 1',
os: OperatingSystem.LINUX,
entries: [
createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'),
createConditionEntry(ConditionEntryField.PATH, '/bin/malware'),
],
};
describe('handlers', () => {
const appContextMock = createAppContextMock();
beforeEach(() => {
exceptionsListClient.deleteExceptionListItem.mockReset();
exceptionsListClient.createExceptionListItem.mockReset();
exceptionsListClient.findExceptionListItem.mockReset();
exceptionsListClient.createTrustedAppsList.mockReset();
appContextMock.logFactory.get.mockClear();
(appContextMock.logFactory.get().error as jest.Mock).mockClear();
});
describe('getTrustedAppsDeleteRouteHandler', () => {
const deleteTrustedAppHandler = getTrustedAppsDeleteRouteHandler(appContextMock);
it('should return ok when trusted app deleted', async () => {
const mockResponse = httpServerMock.createResponseFactory();
exceptionsListClient.deleteExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM);
await deleteTrustedAppHandler(
createHandlerContextMock(),
httpServerMock.createKibanaRequest({ params: { id: '123' } }),
mockResponse
);
assertResponse(mockResponse, 'ok', undefined);
});
it('should return notFound when trusted app missing', async () => {
const mockResponse = httpServerMock.createResponseFactory();
await deleteTrustedAppHandler(
createHandlerContextMock(),
httpServerMock.createKibanaRequest({ params: { id: '123' } }),
mockResponse
);
assertResponse(mockResponse, 'notFound', 'trusted app id [123] not found');
});
it('should return internalError when errors happen', async () => {
const mockResponse = httpServerMock.createResponseFactory();
const error = new Error('Something went wrong');
exceptionsListClient.deleteExceptionListItem.mockRejectedValue(error);
await deleteTrustedAppHandler(
createHandlerContextMock(),
httpServerMock.createKibanaRequest({ params: { id: '123' } }),
mockResponse
);
assertResponse(mockResponse, 'internalError', error);
expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error);
});
});
describe('getTrustedAppsCreateRouteHandler', () => {
const createTrustedAppHandler = getTrustedAppsCreateRouteHandler(appContextMock);
it('should return ok with body when trusted app created', async () => {
const mockResponse = httpServerMock.createResponseFactory();
exceptionsListClient.createExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM);
await createTrustedAppHandler(
createHandlerContextMock(),
httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }),
mockResponse
);
assertResponse(mockResponse, 'ok', { data: TRUSTED_APP });
});
it('should return internalError when errors happen', async () => {
const mockResponse = httpServerMock.createResponseFactory();
const error = new Error('Something went wrong');
exceptionsListClient.createExceptionListItem.mockRejectedValue(error);
await createTrustedAppHandler(
createHandlerContextMock(),
httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }),
mockResponse
);
assertResponse(mockResponse, 'internalError', error);
expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error);
});
});
describe('getTrustedAppsListRouteHandler', () => {
const getTrustedAppsListHandler = getTrustedAppsListRouteHandler(appContextMock);
it('should return ok with list when no errors', async () => {
const mockResponse = httpServerMock.createResponseFactory();
exceptionsListClient.findExceptionListItem.mockResolvedValue({
data: [EXCEPTION_LIST_ITEM],
page: 1,
per_page: 20,
total: 100,
});
await getTrustedAppsListHandler(
createHandlerContextMock(),
httpServerMock.createKibanaRequest({ params: { page: 1, per_page: 20 } }),
mockResponse
);
assertResponse(mockResponse, 'ok', {
data: [TRUSTED_APP],
page: 1,
per_page: 20,
total: 100,
});
});
it('should return internalError when errors happen', async () => {
const mockResponse = httpServerMock.createResponseFactory();
const error = new Error('Something went wrong');
exceptionsListClient.findExceptionListItem.mockRejectedValue(error);
await getTrustedAppsListHandler(
createHandlerContextMock(),
httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }),
mockResponse
);
assertResponse(mockResponse, 'internalError', error);
expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error);
});
});
});

View file

@ -5,16 +5,22 @@
*/
import { RequestHandler, RequestHandlerContext } from 'kibana/server';
import { ExceptionListClient } from '../../../../../lists/server';
import {
DeleteTrustedAppsRequestParams,
GetTrustedAppsListRequest,
GetTrustedListAppsResponse,
PostTrustedAppCreateRequest,
} from '../../../../common/endpoint/types';
import { EndpointAppContext } from '../../types';
import { exceptionItemToTrustedAppItem, newTrustedAppItemToExceptionItem } from './utils';
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants';
import { ExceptionListClient } from '../../../../../lists/server';
import {
createTrustedApp,
deleteTrustedApp,
getTrustedAppsList,
MissingTrustedAppException,
} from './service';
const exceptionListClientFromContext = (context: RequestHandlerContext): ExceptionListClient => {
const exceptionLists = context.lists?.getExceptionListClient();
@ -33,22 +39,16 @@ export const getTrustedAppsDeleteRouteHandler = (
return async (context, req, res) => {
try {
const exceptionsListService = exceptionListClientFromContext(context);
const { id } = req.params;
const response = await exceptionsListService.deleteExceptionListItem({
id,
itemId: undefined,
namespaceType: 'agnostic',
});
if (response === null) {
return res.notFound({ body: `trusted app id [${id}] not found` });
}
await deleteTrustedApp(exceptionListClientFromContext(context), req.params);
return res.ok();
} catch (error) {
logger.error(error);
return res.internalError({ body: error });
if (error instanceof MissingTrustedAppException) {
return res.notFound({ body: `trusted app id [${req.params.id}] not found` });
} else {
logger.error(error);
return res.internalError({ body: error });
}
}
};
};
@ -59,28 +59,10 @@ export const getTrustedAppsListRouteHandler = (
const logger = endpointAppContext.logFactory.get('trusted_apps');
return async (context, req, res) => {
const { page, per_page: perPage } = req.query;
try {
const exceptionsListService = exceptionListClientFromContext(context);
// Ensure list is created if it does not exist
await exceptionsListService.createTrustedAppsList();
const results = await exceptionsListService.findExceptionListItem({
listId: ENDPOINT_TRUSTED_APPS_LIST_ID,
page,
perPage,
filter: undefined,
namespaceType: 'agnostic',
sortField: 'name',
sortOrder: 'asc',
return res.ok({
body: await getTrustedAppsList(exceptionListClientFromContext(context), req.query),
});
const body: GetTrustedListAppsResponse = {
data: results?.data.map(exceptionItemToTrustedAppItem) ?? [],
total: results?.total ?? 0,
page: results?.page ?? 1,
per_page: results?.per_page ?? perPage!,
};
return res.ok({ body });
} catch (error) {
logger.error(error);
return res.internalError({ body: error });
@ -94,21 +76,9 @@ export const getTrustedAppsCreateRouteHandler = (
const logger = endpointAppContext.logFactory.get('trusted_apps');
return async (context, req, res) => {
const newTrustedApp = req.body;
try {
const exceptionsListService = exceptionListClientFromContext(context);
// Ensure list is created if it does not exist
await exceptionsListService.createTrustedAppsList();
const createdTrustedAppExceptionItem = await exceptionsListService.createExceptionListItem(
newTrustedAppItemToExceptionItem(newTrustedApp)
);
return res.ok({
body: {
data: exceptionItemToTrustedAppItem(createdTrustedAppExceptionItem),
},
body: await createTrustedApp(exceptionListClientFromContext(context), req.body),
});
} catch (error) {
logger.error(error);

View file

@ -0,0 +1,425 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CreateExceptionListItemOptions } from '../../../../../lists/server';
import { ExceptionListItemSchema } from '../../../../../lists/common/schemas/response';
import {
ConditionEntryField,
NewTrustedApp,
OperatingSystem,
TrustedApp,
} from '../../../../common/endpoint/types';
import {
createConditionEntry,
createEntryMatch,
createEntryNested,
exceptionListItemToTrustedApp,
newTrustedAppToCreateExceptionListItemOptions,
} from './mapping';
const createExceptionListItemOptions = (
options: Partial<CreateExceptionListItemOptions>
): CreateExceptionListItemOptions => ({
comments: [],
description: '',
entries: [],
itemId: expect.any(String),
listId: 'endpoint_trusted_apps',
meta: undefined,
name: '',
namespaceType: 'agnostic',
osTypes: [],
tags: [],
type: 'simple',
...options,
});
const exceptionListItemSchema = (
item: Partial<ExceptionListItemSchema>
): ExceptionListItemSchema => ({
_version: '123',
id: '',
comments: [],
created_at: '',
created_by: '',
description: '',
entries: [],
item_id: '123',
list_id: 'endpoint_trusted_apps',
meta: undefined,
name: '',
namespace_type: 'agnostic',
os_types: [],
tags: [],
type: 'simple',
tie_breaker_id: '123',
updated_at: '11/11/2011T11:11:11.111',
updated_by: 'admin',
...(item || {}),
});
describe('mapping', () => {
describe('newTrustedAppToCreateExceptionListItemOptions', () => {
const testMapping = (input: NewTrustedApp, expectedResult: CreateExceptionListItemOptions) => {
expect(newTrustedAppToCreateExceptionListItemOptions(input)).toEqual(expectedResult);
};
it('should map linux trusted app condition properly', function () {
testMapping(
{
name: 'linux trusted app',
description: 'Linux Trusted App',
os: OperatingSystem.LINUX,
entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')],
},
createExceptionListItemOptions({
name: 'linux trusted app',
description: 'Linux Trusted App',
osTypes: ['linux'],
entries: [createEntryMatch('process.executable.caseless', '/bin/malware')],
})
);
});
it('should map macos trusted app condition properly', function () {
testMapping(
{
name: 'macos trusted app',
description: 'MacOS Trusted App',
os: OperatingSystem.MAC,
entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')],
},
createExceptionListItemOptions({
name: 'macos trusted app',
description: 'MacOS Trusted App',
osTypes: ['macos'],
entries: [createEntryMatch('process.executable.caseless', '/bin/malware')],
})
);
});
it('should map windows trusted app condition properly', function () {
testMapping(
{
name: 'windows trusted app',
description: 'Windows Trusted App',
os: OperatingSystem.WINDOWS,
entries: [createConditionEntry(ConditionEntryField.PATH, 'C:\\Program Files\\Malware')],
},
createExceptionListItemOptions({
name: 'windows trusted app',
description: 'Windows Trusted App',
osTypes: ['windows'],
entries: [createEntryMatch('process.executable.caseless', 'C:\\Program Files\\Malware')],
})
);
});
it('should map signer condition properly', function () {
testMapping(
{
name: 'Signed trusted app',
description: 'Signed Trusted App',
os: OperatingSystem.WINDOWS,
entries: [createConditionEntry(ConditionEntryField.SIGNER, 'Microsoft Windows')],
},
createExceptionListItemOptions({
name: 'Signed trusted app',
description: 'Signed Trusted App',
osTypes: ['windows'],
entries: [
createEntryNested('process.Ext.code_signature', [
createEntryMatch('trusted', 'true'),
createEntryMatch('subject_name', 'Microsoft Windows'),
]),
],
})
);
});
it('should map MD5 hash condition properly', function () {
testMapping(
{
name: 'MD5 trusted app',
description: 'MD5 Trusted App',
os: OperatingSystem.LINUX,
entries: [
createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'),
],
},
createExceptionListItemOptions({
name: 'MD5 trusted app',
description: 'MD5 Trusted App',
osTypes: ['linux'],
entries: [createEntryMatch('process.hash.md5', '1234234659af249ddf3e40864e9fb241')],
})
);
});
it('should map SHA1 hash condition properly', function () {
testMapping(
{
name: 'SHA1 trusted app',
description: 'SHA1 Trusted App',
os: OperatingSystem.LINUX,
entries: [
createConditionEntry(
ConditionEntryField.HASH,
'f635da961234234659af249ddf3e40864e9fb241'
),
],
},
createExceptionListItemOptions({
name: 'SHA1 trusted app',
description: 'SHA1 Trusted App',
osTypes: ['linux'],
entries: [
createEntryMatch('process.hash.sha1', 'f635da961234234659af249ddf3e40864e9fb241'),
],
})
);
});
it('should map SHA256 hash condition properly', function () {
testMapping(
{
name: 'SHA256 trusted app',
description: 'SHA256 Trusted App',
os: OperatingSystem.LINUX,
entries: [
createConditionEntry(
ConditionEntryField.HASH,
'f635da96124659af249ddf3e40864e9fb234234659af249ddf3e40864e9fb241'
),
],
},
createExceptionListItemOptions({
name: 'SHA256 trusted app',
description: 'SHA256 Trusted App',
osTypes: ['linux'],
entries: [
createEntryMatch(
'process.hash.sha256',
'f635da96124659af249ddf3e40864e9fb234234659af249ddf3e40864e9fb241'
),
],
})
);
});
it('should lowercase hash condition value', function () {
testMapping(
{
name: 'MD5 trusted app',
description: 'MD5 Trusted App',
os: OperatingSystem.LINUX,
entries: [
createConditionEntry(ConditionEntryField.HASH, '1234234659Af249ddf3e40864E9FB241'),
],
},
createExceptionListItemOptions({
name: 'MD5 trusted app',
description: 'MD5 Trusted App',
osTypes: ['linux'],
entries: [createEntryMatch('process.hash.md5', '1234234659af249ddf3e40864e9fb241')],
})
);
});
});
describe('exceptionListItemToTrustedApp', () => {
const testMapping = (input: ExceptionListItemSchema, expectedResult: TrustedApp) => {
expect(exceptionListItemToTrustedApp(input)).toEqual(expectedResult);
};
it('should map linux exception list item properly', function () {
testMapping(
exceptionListItemSchema({
id: '123',
name: 'linux trusted app',
description: 'Linux Trusted App',
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
os_types: ['linux'],
entries: [createEntryMatch('process.executable.caseless', '/bin/malware')],
}),
{
id: '123',
name: 'linux trusted app',
description: 'Linux Trusted App',
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
os: OperatingSystem.LINUX,
entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')],
}
);
});
it('should map macos exception list item properly', function () {
testMapping(
exceptionListItemSchema({
id: '123',
name: 'macos trusted app',
description: 'MacOS Trusted App',
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
os_types: ['macos'],
entries: [createEntryMatch('process.executable.caseless', '/bin/malware')],
}),
{
id: '123',
name: 'macos trusted app',
description: 'MacOS Trusted App',
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
os: OperatingSystem.MAC,
entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')],
}
);
});
it('should map windows exception list item properly', function () {
testMapping(
exceptionListItemSchema({
id: '123',
name: 'windows trusted app',
description: 'Windows Trusted App',
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
os_types: ['windows'],
entries: [createEntryMatch('process.executable.caseless', 'C:\\Program Files\\Malware')],
}),
{
id: '123',
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
name: 'windows trusted app',
description: 'Windows Trusted App',
os: OperatingSystem.WINDOWS,
entries: [createConditionEntry(ConditionEntryField.PATH, 'C:\\Program Files\\Malware')],
}
);
});
it('should map exception list item containing signer entry match properly', function () {
testMapping(
exceptionListItemSchema({
id: '123',
name: 'signed trusted app',
description: 'Signed trusted app',
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
os_types: ['windows'],
entries: [
createEntryNested('process.Ext.code_signature', [
createEntryMatch('trusted', 'true'),
createEntryMatch('subject_name', 'Microsoft Windows'),
]),
],
}),
{
id: '123',
name: 'signed trusted app',
description: 'Signed trusted app',
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
os: OperatingSystem.WINDOWS,
entries: [createConditionEntry(ConditionEntryField.SIGNER, 'Microsoft Windows')],
}
);
});
it('should map exception list item containing MD5 hash entry match properly', function () {
testMapping(
exceptionListItemSchema({
id: '123',
name: 'MD5 trusted app',
description: 'MD5 Trusted App',
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
os_types: ['linux'],
entries: [createEntryMatch('process.hash.md5', '1234234659af249ddf3e40864e9fb241')],
}),
{
id: '123',
name: 'MD5 trusted app',
description: 'MD5 Trusted App',
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
os: OperatingSystem.LINUX,
entries: [
createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'),
],
}
);
});
it('should map exception list item containing SHA1 hash entry match properly', function () {
testMapping(
exceptionListItemSchema({
id: '123',
name: 'SHA1 trusted app',
description: 'SHA1 Trusted App',
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
os_types: ['linux'],
entries: [
createEntryMatch('process.hash.sha1', 'f635da961234234659af249ddf3e40864e9fb241'),
],
}),
{
id: '123',
name: 'SHA1 trusted app',
description: 'SHA1 Trusted App',
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
os: OperatingSystem.LINUX,
entries: [
createConditionEntry(
ConditionEntryField.HASH,
'f635da961234234659af249ddf3e40864e9fb241'
),
],
}
);
});
it('should map exception list item containing SHA256 hash entry match properly', function () {
testMapping(
exceptionListItemSchema({
id: '123',
name: 'SHA256 trusted app',
description: 'SHA256 Trusted App',
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
os_types: ['linux'],
entries: [
createEntryMatch(
'process.hash.sha256',
'f635da96124659af249ddf3e40864e9fb234234659af249ddf3e40864e9fb241'
),
],
}),
{
id: '123',
name: 'SHA256 trusted app',
description: 'SHA256 Trusted App',
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
os: OperatingSystem.LINUX,
entries: [
createConditionEntry(
ConditionEntryField.HASH,
'f635da96124659af249ddf3e40864e9fb234234659af249ddf3e40864e9fb241'
),
],
}
);
});
});
});

View file

@ -0,0 +1,185 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import uuid from 'uuid';
import { OsType } from '../../../../../lists/common/schemas/common';
import {
EntriesArray,
EntryMatch,
EntryNested,
ExceptionListItemSchema,
NestedEntriesArray,
} from '../../../../../lists/common/shared_exports';
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants';
import { CreateExceptionListItemOptions } from '../../../../../lists/server';
import {
ConditionEntry,
ConditionEntryField,
NewTrustedApp,
OperatingSystem,
TrustedApp,
} from '../../../../common/endpoint/types';
type ConditionEntriesMap = { [K in ConditionEntryField]?: ConditionEntry<K> };
type Mapping<T extends string, U> = { [K in T]: U };
const OS_TYPE_TO_OPERATING_SYSTEM: Mapping<OsType, OperatingSystem> = {
linux: OperatingSystem.LINUX,
macos: OperatingSystem.MAC,
windows: OperatingSystem.WINDOWS,
};
const OPERATING_SYSTEM_TO_OS_TYPE: Mapping<OperatingSystem, OsType> = {
[OperatingSystem.LINUX]: 'linux',
[OperatingSystem.MAC]: 'macos',
[OperatingSystem.WINDOWS]: 'windows',
};
const filterUndefined = <T>(list: Array<T | undefined>): T[] => {
return list.filter((item: T | undefined): item is T => item !== undefined);
};
export const createConditionEntry = <T extends ConditionEntryField>(
field: T,
value: string
): ConditionEntry<T> => {
return { field, value, type: 'match', operator: 'included' };
};
export const entriesToConditionEntriesMap = (entries: EntriesArray): ConditionEntriesMap => {
return entries.reduce((result, entry) => {
if (entry.field.startsWith('process.hash') && entry.type === 'match') {
return {
...result,
[ConditionEntryField.HASH]: createConditionEntry(ConditionEntryField.HASH, entry.value),
};
} else if (entry.field === 'process.executable.caseless' && entry.type === 'match') {
return {
...result,
[ConditionEntryField.PATH]: createConditionEntry(ConditionEntryField.PATH, entry.value),
};
} else if (entry.field === 'process.Ext.code_signature' && entry.type === 'nested') {
const subjectNameCondition = entry.entries.find((subEntry): subEntry is EntryMatch => {
return subEntry.field === 'subject_name' && subEntry.type === 'match';
});
if (subjectNameCondition) {
return {
...result,
[ConditionEntryField.SIGNER]: createConditionEntry(
ConditionEntryField.SIGNER,
subjectNameCondition.value
),
};
}
}
return result;
}, {} as ConditionEntriesMap);
};
/**
* Map an ExceptionListItem to a TrustedApp item
* @param exceptionListItem
*/
export const exceptionListItemToTrustedApp = (
exceptionListItem: ExceptionListItemSchema
): TrustedApp => {
if (exceptionListItem.os_types[0]) {
const os = OS_TYPE_TO_OPERATING_SYSTEM[exceptionListItem.os_types[0]];
const grouped = entriesToConditionEntriesMap(exceptionListItem.entries);
return {
id: exceptionListItem.id,
name: exceptionListItem.name,
description: exceptionListItem.description,
created_at: exceptionListItem.created_at,
created_by: exceptionListItem.created_by,
...(os === OperatingSystem.LINUX || os === OperatingSystem.MAC
? {
os,
entries: filterUndefined([
grouped[ConditionEntryField.HASH],
grouped[ConditionEntryField.PATH],
]),
}
: {
os,
entries: filterUndefined([
grouped[ConditionEntryField.HASH],
grouped[ConditionEntryField.PATH],
grouped[ConditionEntryField.SIGNER],
]),
}),
};
} else {
throw new Error('Unknown Operating System assigned to trusted application.');
}
};
const hashType = (hash: string): 'md5' | 'sha256' | 'sha1' | undefined => {
switch (hash.length) {
case 32:
return 'md5';
case 40:
return 'sha1';
case 64:
return 'sha256';
}
};
export const createEntryMatch = (field: string, value: string): EntryMatch => {
return { field, value, type: 'match', operator: 'included' };
};
export const createEntryNested = (field: string, entries: NestedEntriesArray): EntryNested => {
return { field, entries, type: 'nested' };
};
export const conditionEntriesToEntries = (
conditionEntries: Array<ConditionEntry<ConditionEntryField>>
): EntriesArray => {
return conditionEntries.map((conditionEntry) => {
if (conditionEntry.field === ConditionEntryField.HASH) {
return createEntryMatch(
`process.hash.${hashType(conditionEntry.value)}`,
conditionEntry.value.toLowerCase()
);
} else if (conditionEntry.field === ConditionEntryField.SIGNER) {
return createEntryNested(`process.Ext.code_signature`, [
createEntryMatch('trusted', 'true'),
createEntryMatch('subject_name', conditionEntry.value),
]);
} else {
return createEntryMatch(`process.executable.caseless`, conditionEntry.value);
}
});
};
/**
* Map NewTrustedApp to CreateExceptionListItemOptions.
*/
export const newTrustedAppToCreateExceptionListItemOptions = ({
os,
entries,
name,
description = '',
}: NewTrustedApp): CreateExceptionListItemOptions => {
return {
comments: [],
description,
entries: conditionEntriesToEntries(entries),
itemId: uuid.v4(),
listId: ENDPOINT_TRUSTED_APPS_LIST_ID,
meta: undefined,
name,
namespaceType: 'agnostic',
osTypes: [OPERATING_SYSTEM_TO_OS_TYPE[os]],
tags: [],
type: 'simple',
};
};

View file

@ -0,0 +1,123 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ExceptionListItemSchema } from '../../../../../lists/common/schemas/response';
import { listMock } from '../../../../../lists/server/mocks';
import { ExceptionListClient } from '../../../../../lists/server';
import { ConditionEntryField, OperatingSystem } from '../../../../common/endpoint/types';
import { createConditionEntry, createEntryMatch } from './mapping';
import {
createTrustedApp,
deleteTrustedApp,
getTrustedAppsList,
MissingTrustedAppException,
} from './service';
const exceptionsListClient = listMock.getExceptionListClient() as jest.Mocked<ExceptionListClient>;
const EXCEPTION_LIST_ITEM: ExceptionListItemSchema = {
_version: '123',
id: '123',
comments: [],
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
description: 'Linux trusted app 1',
entries: [
createEntryMatch('process.executable.caseless', '/bin/malware'),
createEntryMatch('process.hash.md5', '1234234659af249ddf3e40864e9fb241'),
],
item_id: '123',
list_id: 'endpoint_trusted_apps',
meta: undefined,
name: 'linux trusted app 1',
namespace_type: 'agnostic',
os_types: ['linux'],
tags: [],
type: 'simple',
tie_breaker_id: '123',
updated_at: '11/11/2011T11:11:11.111',
updated_by: 'admin',
};
const TRUSTED_APP = {
id: '123',
created_at: '11/11/2011T11:11:11.111',
created_by: 'admin',
name: 'linux trusted app 1',
description: 'Linux trusted app 1',
os: OperatingSystem.LINUX,
entries: [
createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'),
createConditionEntry(ConditionEntryField.PATH, '/bin/malware'),
],
};
describe('service', () => {
beforeEach(() => {
exceptionsListClient.deleteExceptionListItem.mockReset();
exceptionsListClient.createExceptionListItem.mockReset();
exceptionsListClient.findExceptionListItem.mockReset();
exceptionsListClient.createTrustedAppsList.mockReset();
});
describe('deleteTrustedApp', () => {
it('should delete existing trusted app', async () => {
exceptionsListClient.deleteExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM);
expect(await deleteTrustedApp(exceptionsListClient, { id: '123' })).toBeUndefined();
expect(exceptionsListClient.deleteExceptionListItem).toHaveBeenCalledWith({
id: '123',
namespaceType: 'agnostic',
});
});
it('should throw for non existing trusted app', async () => {
exceptionsListClient.deleteExceptionListItem.mockResolvedValue(null);
await expect(deleteTrustedApp(exceptionsListClient, { id: '123' })).rejects.toBeInstanceOf(
MissingTrustedAppException
);
});
});
describe('createTrustedApp', () => {
it('should create trusted app', async () => {
exceptionsListClient.createExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM);
const result = await createTrustedApp(exceptionsListClient, {
name: 'linux trusted app 1',
description: 'Linux trusted app 1',
os: OperatingSystem.LINUX,
entries: [
createConditionEntry(ConditionEntryField.PATH, '/bin/malware'),
createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'),
],
});
expect(result).toEqual({ data: TRUSTED_APP });
expect(exceptionsListClient.createTrustedAppsList).toHaveBeenCalled();
});
});
describe('getTrustedAppsList', () => {
it('should get trusted apps', async () => {
exceptionsListClient.findExceptionListItem.mockResolvedValue({
data: [EXCEPTION_LIST_ITEM],
page: 1,
per_page: 20,
total: 100,
});
const result = await getTrustedAppsList(exceptionsListClient, { page: 1, per_page: 20 });
expect(result).toEqual({ data: [TRUSTED_APP], page: 1, per_page: 20, total: 100 });
expect(exceptionsListClient.createTrustedAppsList).toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ExceptionListClient } from '../../../../../lists/server';
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants';
import {
DeleteTrustedAppsRequestParams,
GetTrustedAppsListRequest,
GetTrustedListAppsResponse,
PostTrustedAppCreateRequest,
PostTrustedAppCreateResponse,
} from '../../../../common/endpoint/types';
import {
exceptionListItemToTrustedApp,
newTrustedAppToCreateExceptionListItemOptions,
} from './mapping';
export class MissingTrustedAppException {
constructor(public id: string) {}
}
export const deleteTrustedApp = async (
exceptionsListClient: ExceptionListClient,
{ id }: DeleteTrustedAppsRequestParams
) => {
const exceptionListItem = await exceptionsListClient.deleteExceptionListItem({
id,
itemId: undefined,
namespaceType: 'agnostic',
});
if (!exceptionListItem) {
throw new MissingTrustedAppException(id);
}
};
export const getTrustedAppsList = async (
exceptionsListClient: ExceptionListClient,
{ page, per_page: perPage }: GetTrustedAppsListRequest
): Promise<GetTrustedListAppsResponse> => {
// Ensure list is created if it does not exist
await exceptionsListClient.createTrustedAppsList();
const results = await exceptionsListClient.findExceptionListItem({
listId: ENDPOINT_TRUSTED_APPS_LIST_ID,
page,
perPage,
filter: undefined,
namespaceType: 'agnostic',
sortField: 'name',
sortOrder: 'asc',
});
return {
data: results?.data.map(exceptionListItemToTrustedApp) ?? [],
total: results?.total ?? 0,
page: results?.page ?? 1,
per_page: results?.per_page ?? perPage!,
};
};
export const createTrustedApp = async (
exceptionsListClient: ExceptionListClient,
newTrustedApp: PostTrustedAppCreateRequest
): Promise<PostTrustedAppCreateResponse> => {
// Ensure list is created if it does not exist
await exceptionsListClient.createTrustedAppsList();
const createdTrustedAppExceptionItem = await exceptionsListClient.createExceptionListItem(
newTrustedAppToCreateExceptionListItemOptions(newTrustedApp)
);
return { data: exceptionListItemToTrustedApp(createdTrustedAppExceptionItem) };
};

View file

@ -1,522 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EndpointAppContextService } from '../../endpoint_app_context_services';
import {
createMockEndpointAppContext,
createMockEndpointAppContextServiceStartContract,
} from '../../mocks';
import { IRouter, KibanaRequest, RequestHandler } from 'kibana/server';
import { httpServerMock, httpServiceMock } from '../../../../../../../src/core/server/mocks';
import { registerTrustedAppsRoutes } from './index';
import {
TRUSTED_APPS_CREATE_API,
TRUSTED_APPS_DELETE_API,
TRUSTED_APPS_LIST_API,
} from '../../../../common/endpoint/constants';
import {
DeleteTrustedAppsRequestParams,
GetTrustedAppsListRequest,
PostTrustedAppCreateRequest,
} from '../../../../common/endpoint/types';
import { xpackMocks } from '../../../../../../mocks';
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants';
import { EndpointAppContext } from '../../types';
import { ExceptionListClient, ListClient } from '../../../../../lists/server';
import { listMock } from '../../../../../lists/server/mocks';
import {
ExceptionListItemSchema,
FoundExceptionListItemSchema,
} from '../../../../../lists/common/schemas/response';
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
type RequestHandlerContextWithLists = ReturnType<typeof xpackMocks.createRequestHandlerContext> & {
lists?: {
getListClient: () => jest.Mocked<ListClient>;
getExceptionListClient: () => jest.Mocked<ExceptionListClient>;
};
};
describe('when invoking endpoint trusted apps route handlers', () => {
let routerMock: jest.Mocked<IRouter>;
let endpointAppContextService: EndpointAppContextService;
let context: RequestHandlerContextWithLists;
let response: ReturnType<typeof httpServerMock.createResponseFactory>;
let exceptionsListClient: jest.Mocked<ExceptionListClient>;
let endpointAppContext: EndpointAppContext;
beforeEach(() => {
routerMock = httpServiceMock.createRouter();
endpointAppContextService = new EndpointAppContextService();
const startContract = createMockEndpointAppContextServiceStartContract();
exceptionsListClient = listMock.getExceptionListClient() as jest.Mocked<ExceptionListClient>;
endpointAppContextService.start(startContract);
endpointAppContext = {
...createMockEndpointAppContext(),
service: endpointAppContextService,
};
registerTrustedAppsRoutes(routerMock, endpointAppContext);
// For use in individual API calls
context = {
...xpackMocks.createRequestHandlerContext(),
lists: {
getListClient: jest.fn(),
getExceptionListClient: jest.fn().mockReturnValue(exceptionsListClient),
},
};
response = httpServerMock.createResponseFactory();
});
describe('when fetching list of trusted apps', () => {
let routeHandler: RequestHandler<undefined, GetTrustedAppsListRequest>;
const createListRequest = (page: number = 1, perPage: number = 20) => {
return httpServerMock.createKibanaRequest<undefined, GetTrustedAppsListRequest>({
path: TRUSTED_APPS_LIST_API,
method: 'get',
query: {
page,
per_page: perPage,
},
});
};
beforeEach(() => {
// Get the registered List handler from the IRouter instance
[, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith(TRUSTED_APPS_LIST_API)
)!;
});
it('should use ExceptionListClient from route handler context', async () => {
const request = createListRequest();
await routeHandler(context, request, response);
expect(context.lists?.getExceptionListClient).toHaveBeenCalled();
});
it('should create the Trusted Apps List first', async () => {
const request = createListRequest();
await routeHandler(context, request, response);
expect(exceptionsListClient.createTrustedAppsList).toHaveBeenCalled();
expect(response.ok).toHaveBeenCalled();
});
it('should pass pagination query params to exception list service', async () => {
const request = createListRequest(10, 100);
const emptyResponse = {
data: [],
page: 10,
per_page: 100,
total: 0,
};
exceptionsListClient.findExceptionListItem.mockResolvedValue(emptyResponse);
await routeHandler(context, request, response);
expect(response.ok).toHaveBeenCalledWith({ body: emptyResponse });
expect(exceptionsListClient.findExceptionListItem).toHaveBeenCalledWith({
listId: ENDPOINT_TRUSTED_APPS_LIST_ID,
page: 10,
perPage: 100,
filter: undefined,
namespaceType: 'agnostic',
sortField: 'name',
sortOrder: 'asc',
});
});
it('should map Exception List Item to Trusted App item', async () => {
const request = createListRequest(10, 100);
const emptyResponse: FoundExceptionListItemSchema = {
data: [
{
_version: undefined,
comments: [],
created_at: '2020-09-21T19:43:48.240Z',
created_by: 'test',
description: '',
entries: [
{
field: 'process.hash.sha256',
operator: 'included',
type: 'match',
value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476',
},
{
field: 'process.hash.sha1',
operator: 'included',
type: 'match',
value: 'aedb279e378bed6c2db3c9dc9e12ba635e0b391c',
},
{
field: 'process.hash.md5',
operator: 'included',
type: 'match',
value: '741462ab431a22233c787baab9b653c7',
},
],
id: '1',
item_id: '11',
list_id: 'trusted apps test',
meta: undefined,
name: 'test',
namespace_type: 'agnostic',
os_types: ['windows'],
tags: [],
tie_breaker_id: '1',
type: 'simple',
updated_at: '2020-09-21T19:43:48.240Z',
updated_by: 'test',
},
],
page: 10,
per_page: 100,
total: 0,
};
exceptionsListClient.findExceptionListItem.mockResolvedValue(emptyResponse);
await routeHandler(context, request, response);
expect(response.ok).toHaveBeenCalledWith({
body: {
data: [
{
created_at: '2020-09-21T19:43:48.240Z',
created_by: 'test',
description: '',
entries: [
{
field: 'process.hash.*',
operator: 'included',
type: 'match',
value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476',
},
{
field: 'process.hash.*',
operator: 'included',
type: 'match',
value: 'aedb279e378bed6c2db3c9dc9e12ba635e0b391c',
},
{
field: 'process.hash.*',
operator: 'included',
type: 'match',
value: '741462ab431a22233c787baab9b653c7',
},
],
id: '1',
name: 'test',
os: 'windows',
},
],
page: 10,
per_page: 100,
total: 0,
},
});
});
it('should log unexpected error if one occurs', async () => {
exceptionsListClient.findExceptionListItem.mockImplementation(() => {
throw new Error('expected error');
});
const request = createListRequest(10, 100);
await routeHandler(context, request, response);
expect(response.internalError).toHaveBeenCalled();
expect(endpointAppContext.logFactory.get('trusted_apps').error).toHaveBeenCalled();
});
});
describe('when creating a trusted app', () => {
let routeHandler: RequestHandler<undefined, PostTrustedAppCreateRequest>;
const createNewTrustedAppBody = (): {
-readonly [k in keyof PostTrustedAppCreateRequest]: PostTrustedAppCreateRequest[k];
} => ({
name: 'Some Anti-Virus App',
description: 'this one is ok',
os: 'windows',
entries: [
{
field: 'process.executable.caseless',
type: 'match',
operator: 'included',
value: 'c:/programs files/Anti-Virus',
},
],
});
const createPostRequest = (body?: PostTrustedAppCreateRequest) => {
return httpServerMock.createKibanaRequest<undefined, PostTrustedAppCreateRequest>({
path: TRUSTED_APPS_LIST_API,
method: 'post',
body: body ?? createNewTrustedAppBody(),
});
};
beforeEach(() => {
// Get the registered POST handler from the IRouter instance
[, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
path.startsWith(TRUSTED_APPS_CREATE_API)
)!;
// Mock the impelementation of `createExceptionListItem()` so that the return value
// merges in the provided input
exceptionsListClient.createExceptionListItem.mockImplementation(async (newExceptionItem) => {
return ({
...getExceptionListItemSchemaMock(),
...newExceptionItem,
os_types: newExceptionItem.osTypes,
} as unknown) as ExceptionListItemSchema;
});
});
it('should use ExceptionListClient from route handler context', async () => {
const request = createPostRequest();
await routeHandler(context, request, response);
expect(context.lists?.getExceptionListClient).toHaveBeenCalled();
});
it('should create trusted app list first', async () => {
const request = createPostRequest();
await routeHandler(context, request, response);
expect(exceptionsListClient.createTrustedAppsList).toHaveBeenCalled();
expect(response.ok).toHaveBeenCalled();
});
it('should map new trusted app item to an exception list item', async () => {
const request = createPostRequest();
await routeHandler(context, request, response);
expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0]).toEqual({
comments: [],
description: 'this one is ok',
entries: [
{
field: 'process.executable.caseless',
operator: 'included',
type: 'match',
value: 'c:/programs files/Anti-Virus',
},
],
itemId: expect.stringMatching(/.*/),
listId: 'endpoint_trusted_apps',
meta: undefined,
name: 'Some Anti-Virus App',
namespaceType: 'agnostic',
osTypes: ['windows'],
tags: [],
type: 'simple',
});
});
it('should return new trusted app item', async () => {
const request = createPostRequest();
await routeHandler(context, request, response);
expect(response.ok.mock.calls[0][0]).toEqual({
body: {
data: {
created_at: '2020-04-20T15:25:31.830Z',
created_by: 'some user',
description: 'this one is ok',
entries: [
{
field: 'process.executable.caseless',
operator: 'included',
type: 'match',
value: 'c:/programs files/Anti-Virus',
},
],
id: '1',
name: 'Some Anti-Virus App',
os: 'windows',
},
},
});
});
it('should log unexpected error if one occurs', async () => {
exceptionsListClient.createExceptionListItem.mockImplementation(() => {
throw new Error('expected error for create');
});
const request = createPostRequest();
await routeHandler(context, request, response);
expect(response.internalError).toHaveBeenCalled();
expect(endpointAppContext.logFactory.get('trusted_apps').error).toHaveBeenCalled();
});
it('should trim trusted app entry name', async () => {
const newTrustedApp = createNewTrustedAppBody();
newTrustedApp.name = `\n ${newTrustedApp.name} \r\n`;
const request = createPostRequest(newTrustedApp);
await routeHandler(context, request, response);
expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].name).toEqual(
'Some Anti-Virus App'
);
});
it('should trim condition entry values', async () => {
const newTrustedApp = createNewTrustedAppBody();
newTrustedApp.entries.push({
field: 'process.executable.caseless',
value: '\n some value \r\n ',
operator: 'included',
type: 'match',
});
const request = createPostRequest(newTrustedApp);
await routeHandler(context, request, response);
expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([
{
field: 'process.executable.caseless',
operator: 'included',
type: 'match',
value: 'c:/programs files/Anti-Virus',
},
{
field: 'process.executable.caseless',
value: 'some value',
operator: 'included',
type: 'match',
},
]);
});
it('should convert hash values to lowercase', async () => {
const newTrustedApp = createNewTrustedAppBody();
newTrustedApp.entries.push({
field: 'process.hash.*',
value: '741462AB431A22233C787BAAB9B653C7',
operator: 'included',
type: 'match',
});
const request = createPostRequest(newTrustedApp);
await routeHandler(context, request, response);
expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([
{
field: 'process.executable.caseless',
operator: 'included',
type: 'match',
value: 'c:/programs files/Anti-Virus',
},
{
field: 'process.hash.md5',
value: '741462ab431a22233c787baab9b653c7',
operator: 'included',
type: 'match',
},
]);
});
it('should detect md5 hash', async () => {
const newTrustedApp = createNewTrustedAppBody();
newTrustedApp.entries = [
{
field: 'process.hash.*',
value: '741462ab431a22233c787baab9b653c7',
operator: 'included',
type: 'match',
},
];
const request = createPostRequest(newTrustedApp);
await routeHandler(context, request, response);
expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([
{
field: 'process.hash.md5',
value: '741462ab431a22233c787baab9b653c7',
operator: 'included',
type: 'match',
},
]);
});
it('should detect sha1 hash', async () => {
const newTrustedApp = createNewTrustedAppBody();
newTrustedApp.entries = [
{
field: 'process.hash.*',
value: 'aedb279e378bed6c2db3c9dc9e12ba635e0b391c',
operator: 'included',
type: 'match',
},
];
const request = createPostRequest(newTrustedApp);
await routeHandler(context, request, response);
expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([
{
field: 'process.hash.sha1',
value: 'aedb279e378bed6c2db3c9dc9e12ba635e0b391c',
operator: 'included',
type: 'match',
},
]);
});
it('should detect sha256 hash', async () => {
const newTrustedApp = createNewTrustedAppBody();
newTrustedApp.entries = [
{
field: 'process.hash.*',
value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476',
operator: 'included',
type: 'match',
},
];
const request = createPostRequest(newTrustedApp);
await routeHandler(context, request, response);
expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([
{
field: 'process.hash.sha256',
value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476',
operator: 'included',
type: 'match',
},
]);
});
});
describe('when deleting a trusted app', () => {
let routeHandler: RequestHandler<DeleteTrustedAppsRequestParams>;
let request: KibanaRequest<DeleteTrustedAppsRequestParams>;
beforeEach(() => {
[, routeHandler] = routerMock.delete.mock.calls.find(([{ path }]) =>
path.startsWith(TRUSTED_APPS_DELETE_API)
)!;
request = httpServerMock.createKibanaRequest<DeleteTrustedAppsRequestParams>({
path: TRUSTED_APPS_DELETE_API.replace('{id}', '123'),
method: 'delete',
});
});
it('should use ExceptionListClient from route handler context', async () => {
await routeHandler(context, request, response);
expect(context.lists?.getExceptionListClient).toHaveBeenCalled();
});
it('should return 200 on successful delete', async () => {
await routeHandler(context, request, response);
expect(exceptionsListClient.deleteExceptionListItem).toHaveBeenCalledWith({
id: request.params.id,
itemId: undefined,
namespaceType: 'agnostic',
});
expect(response.ok).toHaveBeenCalled();
});
it('should return 404 if item does not exist', async () => {
exceptionsListClient.deleteExceptionListItem.mockResolvedValueOnce(null);
await routeHandler(context, request, response);
expect(response.notFound).toHaveBeenCalled();
});
it('should log unexpected error if one occurs', async () => {
exceptionsListClient.deleteExceptionListItem.mockImplementation(() => {
throw new Error('expected error for delete');
});
await routeHandler(context, request, response);
expect(response.internalError).toHaveBeenCalled();
expect(endpointAppContext.logFactory.get('trusted_apps').error).toHaveBeenCalled();
});
});
});

View file

@ -1,87 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import uuid from 'uuid';
import { ExceptionListItemSchema } from '../../../../../lists/common/shared_exports';
import { NewTrustedApp, TrustedApp } from '../../../../common/endpoint/types';
import { ExceptionListClient } from '../../../../../lists/server';
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants';
type NewExceptionItem = Parameters<ExceptionListClient['createExceptionListItem']>[0];
/**
* Map an ExcptionListItem to a TrustedApp item
* @param exceptionListItem
*/
export const exceptionItemToTrustedAppItem = (
exceptionListItem: ExceptionListItemSchema
): TrustedApp => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { entries, description, created_by, created_at, name, os_types, id } = exceptionListItem;
const os = os_types.length ? os_types[0] : 'unknown';
return {
entries: entries.map((entry) => {
if (entry.field.startsWith('process.hash')) {
return {
...entry,
field: 'process.hash.*',
};
}
return entry;
}),
description,
created_at,
created_by,
name,
os,
id,
} as TrustedApp;
};
export const newTrustedAppItemToExceptionItem = ({
os,
entries,
name,
description = '',
}: NewTrustedApp): NewExceptionItem => {
return {
comments: [],
description,
// @ts-ignore
entries: entries.map(({ value, ...newEntry }) => {
let newValue = value.trim();
if (newEntry.field === 'process.hash.*') {
newValue = newValue.toLowerCase();
newEntry.field = `process.hash.${hashType(newValue)}`;
}
return {
...newEntry,
value: newValue,
};
}),
itemId: uuid.v4(),
listId: ENDPOINT_TRUSTED_APPS_LIST_ID,
meta: undefined,
name: name.trim(),
namespaceType: 'agnostic',
osTypes: [os],
tags: [],
type: 'simple',
};
};
const hashType = (hash: string): 'md5' | 'sha256' | 'sha1' | undefined => {
switch (hash.length) {
case 32:
return 'md5';
case 40:
return 'sha1';
case 64:
return 'sha256';
}
};