-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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 List API #75476
Changes from 13 commits
6defd9c
6f2ce18
a835bca
83739bc
0f4baba
d0b6ea4
0b97949
f7939de
6c94624
fafa9d6
c66077b
697a57b
ae8d876
c717647
6a7fbd6
b97524b
1394a76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,3 +11,5 @@ export const policyIndexPattern = 'metrics-endpoint.policy-*'; | |
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_LIST_API = '/api/endpoint/trusted_apps'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why /endpoint? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. other endpoint apis are mounted at this location |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
/* | ||
* 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 { GetTrustedAppsRequestSchema } from './trusted_apps'; | ||
|
||
describe('When invoking Trusted Apps Schema', () => { | ||
describe('for GET List', () => { | ||
const getListQueryParams = (page: unknown = 1, perPage: unknown = 20) => ({ | ||
page, | ||
per_page: perPage, | ||
}); | ||
const query = GetTrustedAppsRequestSchema.query; | ||
|
||
describe('query param validation', () => { | ||
it('should return query params if valid', () => { | ||
expect(query.validate(getListQueryParams())).toEqual({ | ||
page: 1, | ||
per_page: 20, | ||
}); | ||
}); | ||
|
||
it('should use default values', () => { | ||
expect(query.validate(getListQueryParams(undefined, undefined))).toEqual({ | ||
page: 1, | ||
per_page: 20, | ||
}); | ||
expect(query.validate(getListQueryParams(undefined, 100))).toEqual({ | ||
page: 1, | ||
per_page: 100, | ||
}); | ||
expect(query.validate(getListQueryParams(10, undefined))).toEqual({ | ||
page: 10, | ||
per_page: 20, | ||
}); | ||
}); | ||
|
||
it('should throw if `page` param is not a number', () => { | ||
expect(() => { | ||
query.validate(getListQueryParams('one')); | ||
}).toThrowError(); | ||
}); | ||
|
||
it('should throw if `page` param is less than 1', () => { | ||
expect(() => { | ||
query.validate(getListQueryParams(0)); | ||
}).toThrowError(); | ||
expect(() => { | ||
query.validate(getListQueryParams(-1)); | ||
}).toThrowError(); | ||
}); | ||
|
||
it('should throw if `per_page` param is not a number', () => { | ||
expect(() => { | ||
query.validate(getListQueryParams(1, 'twenty')); | ||
}).toThrowError(); | ||
}); | ||
|
||
it('should throw if `per_page` param is less than 1', () => { | ||
expect(() => { | ||
query.validate(getListQueryParams(1, 0)); | ||
}).toThrowError(); | ||
expect(() => { | ||
query.validate(getListQueryParams(1, -1)); | ||
}).toThrowError(); | ||
}); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
/* | ||
* 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 { schema } from '@kbn/config-schema'; | ||
|
||
export const GetTrustedAppsRequestSchema = { | ||
query: schema.object({ | ||
page: schema.maybe(schema.number({ defaultValue: 1, min: 1 })), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I always assumed 0 based is easier to implement, but maybe it's true that we need to show one based. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm of the opposite opinion - I find it 1 based to be clearer from a consumer (UI?) standpoint. I want page 1. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
per_page: schema.maybe(schema.number({ defaultValue: 20, min: 1 })), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just out of curiosity and not for bikesheding - why not page_size? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mirror of lists API - they use these values. I also seem to remember that anything outside of code should use |
||
}), | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* | ||
* 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 { TypeOf } from '@kbn/config-schema'; | ||
import { GetTrustedAppsRequestSchema } from '../schema/trusted_apps'; | ||
import { ExceptionListItemSchema } from '../../../../lists/common'; | ||
import { FoundExceptionListItemSchema } from '../../../../lists/common/schemas/response'; | ||
|
||
/** API request params for retrieving a list of Trusted Apps */ | ||
export type GetTrustedAppsListRequest = TypeOf<typeof GetTrustedAppsRequestSchema.query>; | ||
export type GetTrustedListAppsResponse = Pick< | ||
FoundExceptionListItemSchema, | ||
'per_page' | 'page' | 'total' | ||
> & { | ||
data: TrustedApp[]; | ||
}; | ||
|
||
/** Type for a new Trusted App Entry */ | ||
export type NewTrustedApp = Pick<ExceptionListItemSchema, 'name' | 'description' | 'entries'> & { | ||
os: string; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It feels like enum would be more appropriate. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it will probably be an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we could introduce an enum with with the Create flow and validation There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have a ticket to add the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we can create the Enum either with the UI changes or when we add the Create api. @peluja1012 sounds good. Madi had mentioned that to me, so we're aware we will have to adjust our "translator" once that gets introduced. |
||
}; | ||
|
||
/** A trusted app entry */ | ||
export type TrustedApp = NewTrustedApp & | ||
Pick<ExceptionListItemSchema, 'created_at' | 'created_by'> & { | ||
id: string; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
#!/usr/bin/env node | ||
/* | ||
* 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. | ||
*/ | ||
|
||
require('../../../../../src/setup_node_env'); | ||
require('./trusted_apps').cli(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI - this utility is to assist with dev. of the Trusted Apps list until we have a |
||
* 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 { v4 as generateUUID } from 'uuid'; | ||
// @ts-ignore | ||
import minimist from 'minimist'; | ||
import { KbnClient, ToolingLog } from '@kbn/dev-utils'; | ||
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../lists/common/constants'; | ||
import { TRUSTED_APPS_LIST_API } from '../../../common/endpoint/constants'; | ||
import { ExceptionListItemSchema } from '../../../../lists/common/schemas/response'; | ||
|
||
interface RunOptions { | ||
count?: number; | ||
} | ||
|
||
const logger = new ToolingLog({ level: 'info', writeTo: process.stdout }); | ||
const separator = '----------------------------------------'; | ||
|
||
export const cli = async () => { | ||
const options: RunOptions = minimist(process.argv.slice(2), { | ||
default: { | ||
count: 10, | ||
}, | ||
}); | ||
logger.write(`${separator} | ||
Loading ${options.count} Trusted App Entries`); | ||
await run(options); | ||
logger.write(`Done! | ||
${separator}`); | ||
}; | ||
|
||
export const run: (options?: RunOptions) => Promise<ExceptionListItemSchema[]> = async ({ | ||
count = 10, | ||
}: RunOptions = {}) => { | ||
const kbnClient = new KbnClient(logger, { url: 'http://elastic:changeme@localhost:5601' }); | ||
|
||
// touch the Trusted Apps List so it can be created | ||
await kbnClient.request({ | ||
method: 'GET', | ||
path: TRUSTED_APPS_LIST_API, | ||
}); | ||
|
||
return Promise.all( | ||
Array.from({ length: count }, () => { | ||
return kbnClient | ||
.request({ | ||
method: 'POST', | ||
path: '/api/exception_lists/items', | ||
body: generateTrustedAppEntry(), | ||
}) | ||
.then<ExceptionListItemSchema>((item) => (item as unknown) as ExceptionListItemSchema); | ||
}) | ||
); | ||
}; | ||
|
||
interface GenerateTrustedAppEntryOptions { | ||
os?: 'windows' | 'macos' | 'linux'; | ||
name?: string; | ||
} | ||
|
||
const generateTrustedAppEntry: (options?: GenerateTrustedAppEntryOptions) => object = ({ | ||
os = 'windows', | ||
name = `Sample Endpoint Trusted App Entry ${Date.now()}`, | ||
} = {}) => { | ||
return { | ||
list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, | ||
item_id: `generator_endpoint_trusted_apps_${generateUUID()}`, | ||
_tags: ['endpoint', `os:${os}`], | ||
tags: ['user added string for a tag', 'malware'], | ||
type: 'simple', | ||
description: 'This is a sample agnostic endpoint trusted app entry', | ||
name, | ||
namespace_type: 'agnostic', | ||
entries: [ | ||
{ | ||
field: 'actingProcess.file.signer', | ||
operator: 'included', | ||
type: 'match', | ||
value: 'Elastic, N.V.', | ||
}, | ||
{ | ||
field: 'actingProcess.file.path', | ||
operator: 'included', | ||
type: 'match', | ||
value: '/one/two/three', | ||
}, | ||
], | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
/* | ||
* 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 { RequestHandler } from 'kibana/server'; | ||
import { | ||
GetTrustedAppsListRequest, | ||
GetTrustedListAppsResponse, | ||
} from '../../../../common/endpoint/types'; | ||
import { EndpointAppContext } from '../../types'; | ||
import { exceptionItemToTrustedAppItem } from './utils'; | ||
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; | ||
|
||
export const getTrustedAppsListRouteHandler = ( | ||
endpointAppContext: EndpointAppContext | ||
): RequestHandler<undefined, GetTrustedAppsListRequest> => { | ||
paul-tavares marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const logger = endpointAppContext.logFactory.get('trusted_apps'); | ||
|
||
return async (context, req, res) => { | ||
const exceptionsListService = endpointAppContext.service.getExceptionsList(); | ||
const { page, per_page: perPage } = req.query; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is why I'm wondering if it's a convention to follow "_" naming style for REST :) |
||
|
||
try { | ||
// 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', | ||
}); | ||
const body: GetTrustedListAppsResponse = { | ||
data: results?.data.map(exceptionItemToTrustedAppItem) ?? [], | ||
total: results?.total ?? 0, | ||
page: results?.page ?? 1, | ||
per_page: perPage!, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No default? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perPage default to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also this value is already known to the client when making request, why send it back? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see other APIs "echo" the perPage count back. We can remove it if you feel strongly about it, but I was just trying to remain consistent |
||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what @nnamdifrankie and @pzl have to say, but I would personally always introduce the service layer function to wrap the call to exceptionsListService. My rule of thumb is to have controller call service from the same logical domain, that scales better in terms of future evolvement. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can change the |
||
return res.ok({ body }); | ||
} catch (error) { | ||
logger.error(error); | ||
return res.internalError({ body: error }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure do we follow REST conventions for response codes? I can imagine there are errors that are client errors that would trigger 400 or 403. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if you come here, we don't know if its a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we can prove for sure it was a client error then it should definitely be 4xx error. And I believe a few of those cases are already caught (not authorized: 403, due to If we can't prove it's a request fault, better to say 'excuse me' (5xx) than "I can't believe you've done this" (4xx) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can revisit this later to see if it needs more interpretation. but given what is inside of the |
||
} | ||
}; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@madirey FYI
Created this so that the client mock (below) can include it. I debated whether I should have created a sub-class of the Client under trusted apps and felt it belonged here more than in our feature code