mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
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:
parent
b99abe301a
commit
de5edaa278
30 changed files with 1435 additions and 1042 deletions
|
@ -23,6 +23,7 @@ export {
|
|||
EntryList,
|
||||
EntriesArray,
|
||||
NamespaceType,
|
||||
NestedEntriesArray,
|
||||
Operator,
|
||||
OperatorEnum,
|
||||
OperatorTypeEnum,
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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}';
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
])
|
||||
),
|
||||
]),
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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 & {
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: [],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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'
|
||||
),
|
||||
],
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
};
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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) };
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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';
|
||||
}
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue