Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SECURITY_SOLUTION][ENDPOINT] Trusted Apps Create API #76178

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurre
export const LIMITED_CONCURRENCY_ENDPOINT_COUNT = 100;

export const TRUSTED_APPS_LIST_API = '/api/endpoint/trusted_apps';
export const TRUSTED_APPS_CREATE_API = '/api/endpoint/trusted_apps';
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { GetTrustedAppsRequestSchema } from './trusted_apps';
import { GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema } from './trusted_apps';

describe('When invoking Trusted Apps Schema', () => {
describe('for GET List', () => {
Expand Down Expand Up @@ -68,4 +68,180 @@ describe('When invoking Trusted Apps Schema', () => {
});
});
});

describe('for POST Create', () => {
const getCreateTrustedAppItem = () => ({
name: 'Some Anti-Virus App',
description: 'this one is ok',
os: 'windows',
entries: [
{
field: 'path',
type: 'match',
operator: 'included',
value: 'c:/programs files/Anti-Virus',
},
],
});
const body = PostTrustedAppCreateRequestSchema.body;

it('should not error on a valid message', () => {
const bodyMsg = getCreateTrustedAppItem();
expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg);
});

it('should validate `name` is required', () => {
const bodyMsg = {
...getCreateTrustedAppItem(),
name: undefined,
};
expect(() => body.validate(bodyMsg)).toThrow();
});

it('should validate `name` value to be non-empty', () => {
const bodyMsg = {
...getCreateTrustedAppItem(),
name: '',
};
expect(() => body.validate(bodyMsg)).toThrow();
});

it('should validate `description` as optional', () => {
const { description, ...bodyMsg } = getCreateTrustedAppItem();
expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg);
});

it('should validate `description` to be non-empty if defined', () => {
const bodyMsg = {
...getCreateTrustedAppItem(),
description: '',
};
expect(() => body.validate(bodyMsg)).toThrow();
});

it('should validate `os` to to only accept known values', () => {
const bodyMsg = {
...getCreateTrustedAppItem(),
os: undefined,
};
expect(() => body.validate(bodyMsg)).toThrow();

const bodyMsg2 = {
...bodyMsg,
os: '',
};
expect(() => body.validate(bodyMsg2)).toThrow();

const bodyMsg3 = {
...bodyMsg,
os: 'winz',
};
expect(() => body.validate(bodyMsg3)).toThrow();

['linux', 'macos', '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();

const { entries, ...bodyMsg2 } = getCreateTrustedAppItem();
expect(() => body.validate(bodyMsg2)).toThrow();
});

it('should validate `entries` to have at least 1 item', () => {
const bodyMsg = {
...getCreateTrustedAppItem(),
entries: [],
};
expect(() => body.validate(bodyMsg)).toThrow();
});

describe('when `entries` are defined', () => {
const getTrustedAppItemEntryItem = () => getCreateTrustedAppItem().entries[0];

it('should validate `entry.field` is required', () => {
const { field, ...entry } = getTrustedAppItemEntryItem();
const bodyMsg = {
...getCreateTrustedAppItem(),
entries: [entry],
};
expect(() => body.validate(bodyMsg)).toThrow();
});

it('should validate `entry.field` is limited to known values', () => {
const bodyMsg = {
...getCreateTrustedAppItem(),
entries: [
{
...getTrustedAppItemEntryItem(),
field: '',
},
],
};
expect(() => body.validate(bodyMsg)).toThrow();

const bodyMsg2 = {
...getCreateTrustedAppItem(),
entries: [
{
...getTrustedAppItemEntryItem(),
field: 'invalid value',
},
],
};
expect(() => body.validate(bodyMsg2)).toThrow();

['hash', 'path'].forEach((field) => {
const bodyMsg3 = {
...getCreateTrustedAppItem(),
entries: [
{
...getTrustedAppItemEntryItem(),
field,
},
],
};

expect(() => body.validate(bodyMsg3)).not.toThrow();
});
});

it.todo('should validate `entry.type` is limited to known values');

it.todo('should validate `entry.operator` is limited to known values');

it('should validate `entry.value` required', () => {
const { value, ...entry } = getTrustedAppItemEntryItem();
const bodyMsg = {
...getCreateTrustedAppItem(),
entries: [entry],
};
expect(() => body.validate(bodyMsg)).toThrow();
});

it('should validate `entry.value` is non-empty', () => {
const bodyMsg = {
...getCreateTrustedAppItem(),
entries: [
{
...getTrustedAppItemEntryItem(),
value: '',
},
],
};
expect(() => body.validate(bodyMsg)).toThrow();
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,20 @@ export const GetTrustedAppsRequestSchema = {
per_page: schema.maybe(schema.number({ defaultValue: 20, min: 1 })),
}),
};

export const PostTrustedAppCreateRequestSchema = {
body: schema.object({
name: schema.string({ minLength: 1 }),
description: schema.maybe(schema.string({ minLength: 1 })),
os: schema.oneOf([schema.literal('linux'), schema.literal('macos'), schema.literal('windows')]),
entries: schema.arrayOf(
schema.object({
field: schema.oneOf([schema.literal('hash'), schema.literal('path')]),
type: schema.literal('match'),
operator: schema.literal('included'),
value: schema.string({ minLength: 1 }),
}),
{ minSize: 1 }
),
}),
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
*/

import { TypeOf } from '@kbn/config-schema';
import { GetTrustedAppsRequestSchema } from '../schema/trusted_apps';
import {
GetTrustedAppsRequestSchema,
PostTrustedAppCreateRequestSchema,
} from '../schema/trusted_apps';

/** API request params for retrieving a list of Trusted Apps */
export type GetTrustedAppsListRequest = TypeOf<typeof GetTrustedAppsRequestSchema.query>;
Expand All @@ -16,6 +19,12 @@ export interface GetTrustedListAppsResponse {
data: TrustedApp[];
}

/** API Request body for creating a new Trusted App entry */
export type PostTrustedAppCreateRequest = TypeOf<typeof PostTrustedAppCreateRequestSchema.body>;
export interface PostTrustedAppCreateResponse {
data: TrustedApp;
}

interface MacosLinuxConditionEntry {
field: 'hash' | 'path';
type: 'match';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { RequestHandler } from 'kibana/server';
import {
GetTrustedAppsListRequest,
GetTrustedListAppsResponse,
PostTrustedAppCreateRequest,
} from '../../../../common/endpoint/types';
import { EndpointAppContext } from '../../types';
import { exceptionItemToTrustedAppItem } from './utils';
import { exceptionItemToTrustedAppItem, newTrustedAppItemToExceptionItem } from './utils';
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants';

export const getTrustedAppsListRouteHandler = (
Expand All @@ -24,7 +25,7 @@ export const getTrustedAppsListRouteHandler = (

try {
// Ensure list is created if it does not exist
await exceptionsListService?.createTrustedAppsList();
await exceptionsListService.createTrustedAppsList();
const results = await exceptionsListService.findExceptionListItem({
listId: ENDPOINT_TRUSTED_APPS_LIST_ID,
page,
Expand All @@ -47,3 +48,32 @@ export const getTrustedAppsListRouteHandler = (
}
};
};

export const getTrustedAppsCreateRouteHandler = (
endpointAppContext: EndpointAppContext
): RequestHandler<undefined, undefined, PostTrustedAppCreateRequest> => {
const logger = endpointAppContext.logFactory.get('trusted_apps');

return async (constext, req, res) => {
const exceptionsListService = endpointAppContext.service.getExceptionsList();
const newTrustedApp = req.body;

try {
// 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),
},
});
} catch (error) {
logger.error(error);
return res.internalError({ body: error });
}
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@
*/

import { IRouter } from 'kibana/server';
import { GetTrustedAppsRequestSchema } from '../../../../common/endpoint/schema/trusted_apps';
import { TRUSTED_APPS_LIST_API } from '../../../../common/endpoint/constants';
import { getTrustedAppsListRouteHandler } from './handlers';
import {
GetTrustedAppsRequestSchema,
PostTrustedAppCreateRequestSchema,
} from '../../../../common/endpoint/schema/trusted_apps';
import {
TRUSTED_APPS_CREATE_API,
TRUSTED_APPS_LIST_API,
} from '../../../../common/endpoint/constants';
import { getTrustedAppsCreateRouteHandler, getTrustedAppsListRouteHandler } from './handlers';
import { EndpointAppContext } from '../../types';

export const registerTrustedAppsRoutes = (
Expand All @@ -23,4 +29,14 @@ export const registerTrustedAppsRoutes = (
},
getTrustedAppsListRouteHandler(endpointAppContext)
);

// CREATE
router.post(
{
path: TRUSTED_APPS_CREATE_API,
validate: PostTrustedAppCreateRequestSchema,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the schema doesn't validate here, we get a 400 error? Do we get a meaningful message from the error? From your tests, it looks like an error will be thrown as expected - expect(() => body.validate(bodyMsg)).toThrow() - just wondering what the error actually looks like

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they are "typical" errors thrown by the schema lib. Here are a few examples:

// entries was an empty array
{
  'statusCode': 400,
  'error': 'Bad Request',
  'message': '[request body.entries]: array size is [0], but cannot be smaller than [1]',
}
// enty had os set to `winz`
{
  'statusCode': 400,
  'error': 'Bad Request',
  'message': '[request body.os]: types that failed validation:\n- [request body.os.0]: expected value to equal [linux]\n- [request body.os.1]: expected value to equal [macos]\n- [request body.os.2]: expected value to equal [windows]',
}

options: { authRequired: true },
},
getTrustedAppsCreateRouteHandler(endpointAppContext)
);
};
Loading