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 List API #75476

Merged

Conversation

paul-tavares
Copy link
Contributor

@paul-tavares paul-tavares commented Aug 19, 2020

Summary

Introduce new API for retrieving list of Trusted Apps. Also includes a script for loading Trusted Apps entries while the Create API is unavailable (may delete that script once that is available).

In addition the endpoint types under common were moved to a directory named types in order to be able to add separate type files per domain (ex. trusted_apps types)

Request:

  • API route: GET /api/endpoint/trusted_apps
  • Query Params:
{
    page: number;
    per_page: number;
}
  • Results will be sorted by name in ascending order

Response:

{
    data: Array<TrustedApp>;
    page: number;
    per_page: number;
    total: number;
}

image

Checklist

Delete any items that are not applicable to this PR.

@paul-tavares paul-tavares added v8.0.0 release_note:skip Skip the PR/issue when compiling release notes Team:Endpoint Management Feature:Endpoint Elastic Endpoint feature v7.10.0 labels Aug 19, 2020
@paul-tavares paul-tavares self-assigned this Aug 19, 2020
export const GetTrustedAppsRequestSchema = {
query: schema.object({
page: schema.maybe(schema.number({ defaultValue: 1, min: 1 })),
per_page: schema.maybe(schema.number({ defaultValue: 20, min: 1 })),
Copy link
Contributor

Choose a reason for hiding this comment

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

Just out of curiosity and not for bikesheding - why not page_size?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 _ and only code vars/etc. should camelCase. I think Kibana even has an ESLint rule for it.


export const GetTrustedAppsRequestSchema = {
query: schema.object({
page: schema.maybe(schema.number({ defaultValue: 1, min: 1 })),
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
The service is 1 based and FYI - we have an issue opened to also refactor Endpoint hosts page to use 1 based pagination.

Copy link
Member

Choose a reason for hiding this comment

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

+1 to 1-based page numbers.

2020-08-25-123057_scrot

I consider the URL-bar to be human-facing as much as this

@@ -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';
Copy link
Contributor

Choose a reason for hiding this comment

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

why /endpoint?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

other endpoint apis are mounted at this location

ExceptionListItemSchema,
'created_at' | 'created_by' | 'name' | 'description' | 'entries'
> & {
os: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

It feels like enum would be more appropriate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it will probably be an enum - just need to find the one that List is using (if one exists) and reuse it or come up with our own. FYI: this os field does not exist today on the data storage schema - the Trusted Apps API will populate it from the _tags[] array.

Copy link
Contributor

Choose a reason for hiding this comment

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

we could introduce an enum with with the Create flow and validation

Copy link
Contributor

Choose a reason for hiding this comment

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

We have a ticket to add the os to the exception list item schema in 7.10

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

/** Type for a new Trusted App Entry */
export type NewTrustedApp = Pick<
ExceptionListItemSchema,
'created_at' | 'created_by' | 'name' | 'description' | 'entries'
Copy link
Contributor

Choose a reason for hiding this comment

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

A question and a comment here:

Question: Are we not using camel casing for APIs?

Remark: So sending entries means that we will have to have logic on the client that will have to translate the entries into a single field applicationPath. My personal view on it is that it's unnecessarily complicates client code without good reason - it's more code, it will have to handle error cases (like empty list or missing item with needed field name), it exposes internals (like field names) to the client. In addition it might trigger users to explore what they can submit through that loop hole and it makes it less straight forward to document this API (again binding ourselves to internal event field names that might change, and it's not very obvious that this frontend code should evolve along with the events code).

I really would propose to not use Pick<...> and couple ourselves to how ExceptionListItemSchema is defined but just define our own interface for API data structure. This has multiple benefits:

  • Even from readability point of view it's way more straight forward and readable. Currently I'll have to navigate to ExceptionListItemSchema to know the types of the fields, which is not very convenient.
  • We have a freedom to use names that more convenient for our view
  • It will prevent accidental leak through - assume the case we change created_at from timestamp string to milliseconds number, that means that the code compiles but frontend fails, while having own data structure and translation code will fail on compile.
  • We don't add complexity to the frontend but rather keep it far away from it.

As mentioned this translation will have to happen somewhere and my experience suggests that it's best to push it as much back in the layers as possible, otherwise the complexity of this data structure complicates all layers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

re: casing - it comes down to what I have seen around in kibana: non code references are camelCased, everything else is snake_cased.

Re:

So sending entries means that we will have to have logic on the client that will have to translate the entries into a single field applicationPath.

Not sure I follow - why would you need logic on the client to translate?

Re: Type and pick from Exceptions
I think our data types should be defined to match the data storage as close as possible. In this case, because we (trusted apps and Exceptions) share the same storage schema, I feel its best to pick the properties that we will reuse from that schema, so that it it changes in the future to something that we don't support, we can get those (TS) errors surfaced at the earlier possible time. This is also consistent with usage patterns I have seen across kibana.

// FIXME:PT call create list once PR merges

try {
const results = await exceptionsListService?.findExceptionListItem({
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, the exceptionListService is optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was surprise as well. I may replace ? with !.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I fixed it by changing the EndpointAppContextService.getExceptionList()

const logger = endpointAppContext.logFactory.get('trusted_apps');

return async (context, req, res) => {
const { page, per_page: perPage } = req.query;
Copy link
Contributor

Choose a reason for hiding this comment

The 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 :)

export const exceptionItemToTrustedAppItem = (
exceptionListItem: ExceptionListItemSchema
): TrustedApp => {
// eslint-disable-next-line @typescript-eslint/naming-convention
Copy link
Contributor

Choose a reason for hiding this comment

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

Same point about "_" namings in REST, maybe don't use destruction instead?

return tag;
}
}
return '';
Copy link
Contributor

Choose a reason for hiding this comment

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

I know it's not "supposed" to happen, but I'd rather fail here than hide the error, there is no meaningful handling of this in UI.

This is always a problem of using generic structures to represent more specific concepts - they leak through and break invariance. Not trying to trigger same discussion again :D just it would be so nicer to just use SavedObject ;)

Actually again a bit on the topic of our own API interface instead of inheriting ExceptionListItem - we now scatter transformations: OS is extracted on the server while path/hash/signer will have to be extracted on the client. I'd again suggest to not send entries but rather translate it to the data structure we can directly consume in UI. This is also why I mentioned in grooming that I would not incorporate signer in API for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right in that this should NOT happen. But if for some reason that is true, then we are essentially making the list API (and LIST UI) basically unusable. (API will never return if at least 1 entry has no OS.

Copy link
Contributor

Choose a reason for hiding this comment

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

could we have it return unknown at least so the UI could use it if needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok. I will set it to a string unknown, but that will likely cause issues once we introduce a type for it like an enum.

Copy link
Member

Choose a reason for hiding this comment

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

'unknown' feels like a magic string waiting to happen (until it is an explicit enum case). Magic strings should not happen from the server. If the front-end ever wanted to display missing OS's differently (like a blank column), one would have to filter for the string value, which is a bit code-smelly. I didn't look up the call chain where this is used, but can this return string | null? The missing case, being null. That would perhaps offer the front-end an opportunity to display/handle explicitly. At the cost of null-checking, which also isn't fun, but more common.

Stop me if this is anti-typescript behavior.

@efreeti
Copy link
Contributor

efreeti commented Aug 20, 2020

So again on the topic of our own interface for return types from API - I think we should go with that option as it's justifies having own API, otherwise it's a bit half way solution and has less justification for separate API.

In the end think of it like this - we should try to encode as much as possible invariance into API structures so that client has to face way less ambiguity and handle weird invalid cases (like empty OS, empty entries, missing known entries, etc.). I think mostly you already do it and I would suggest to do it also for "entries" field: replace it with data structure that preserves invariance for trusted app matching that we expect in UI.

For example exposing field in entries will not force people's code to fail if they try to send matching signer for Linux.

As a thought I would go even further:

interface WindowsTrustedAppConditions {
  platform: 'windows';
  signer?: string;
  hash?: string;
  path?: string;
}

interface MacTrustedAppConditions {
  platform: 'mac';
  hash?: string;
  path?: string;
}

interface LinuxTrustedAppConditions {
  platform: 'linux';
  hash?: string;
  path?: string;
}

type TrustedAppConditions = WindowsTrustedAppConditions | MacTrustedAppConditions | LinuxTrustedAppConditions;

interface TrustedApp {
  id: string;
  name: string;
  description: string;
  createdAt: Date;
  createdBy: string;
  conditions: TrustedAppConditions;
}

@paul-tavares , @kevinlog , @pzl , @nnamdifrankie what do you guys think? The above could really provide a high level of invariance on the domain entity that we manipulate.

@paul-tavares
Copy link
Contributor Author

@efreeti
I think we are in sync with the exception that you would like to call it conditions and I'm advocating for keeping the same entries property name. I find it to be an unnecessary conversion to be done at the API level or UI because it does not bubble up to the user. They are just entries that defined the conditions for the "exception". It also matches the internal field that is used to send the data down to the endpoint, so does (IMO) help sync thing up for a dev. Also, keeping the same name of this property will assist with possibly re-using the current code in the Manifest Manager component to create the actual artifact, so that we don't have to come up with our own translator there too.

Re: the types you defined above
I'm not sure that lines up with what needs to be supported in the UI. I don't think you can capture the os inside of the condition, because it is something you define at the "exception rule" level, which then dictates what you can store inside of the entries[] array. Might be able to defined multiple Trusted Apps types - that would work. Example:

interface MacosLinuxConditionEntry {
  field: 'hash' | 'path';
  type: 'match';
  operator: 'included';
  value: string;
}

type WindowsConditionEntry = MacosLinuxConditionEntry & {
  field: 'signer';
};

type TrustedApp =
  | {
      id: string;
      name: string;
      description: string;
      createdAt: Date;
      createdBy: string;
      os: 'windows';
      entries: WindowsConditionEntry[];
    }
  | {
      id: string;
      name: string;
      description: string;
      createdAt: Date;
      createdBy: string;
      os: 'linux' | 'macos';
      entries: MacosLinuxConditionEntry[];
    };

We can remove the type and operator from each entry, since this will not be supported via the UI and could have the API automatically add that in on create.

With the above structure, you would get type guards against invalid combinations:

image

@@ -0,0 +1,91 @@
/*
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 create flow. I would expect this to be deleted once the create flow is merged.

@paul-tavares paul-tavares marked this pull request as ready for review August 24, 2020 18:32
@paul-tavares paul-tavares requested review from a team as code owners August 24, 2020 18:32
@elasticmachine
Copy link
Contributor

Pinging @elastic/endpoint-app-team (Feature:Endpoint)

@elasticmachine
Copy link
Contributor

Pinging @elastic/endpoint-management (Team:Endpoint Management)

@@ -42,6 +47,15 @@ export const getExceptionListSchemaMock = (): ExceptionListSchema => ({
version: VERSION,
});

export const getTrustedAppsListSchemaMock = (): ExceptionListSchema => {
Copy link
Contributor Author

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

* Map an ExcptionListItem to a TrustedApp item
* @param exceptionListItem
*/
export const exceptionItemToTrustedAppItem = (
Copy link
Contributor

Choose a reason for hiding this comment

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

this is good and can help show how we use Exceptions list

Copy link
Contributor

@kevinlog kevinlog left a comment

Choose a reason for hiding this comment

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

just a couple comments. I'm good with the direction that we agreed on with @peluja1012 and @madirey team to base our structure on Exceptions. The exceptionItemToTrustedAppItem and type are good ways to communicate that relationship.

// GET list
router.get(
{
path: TRUSTED_APPS_LIST_API,
Copy link
Contributor

Choose a reason for hiding this comment

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

I haven't seen other server code creating such constants.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wish they did. Both the UI and the Server can benefit from such constants since both need it. Ingest uses this heavily and it helps allot (ex. we import some of their consts and use it in our API calls - thus if they change, we will not break).

This const should be re-used by the UI's middleware when making the API call.

Copy link
Member

Choose a reason for hiding this comment

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

@efreeti don't treat the existing endpoint/management server code as stylistic gospel. It was developed while we were still new to convention, and right after basic POCs.

An argument can be made to "match what's there" not because it's better but just for the sake of consistency. I can accept that point. Then if we want to change the style for all, it's an easier task if they all have the same pattern to -> from.

However I also like and prefer this idea. We should just have a tech debt item to change the existing patterns to have const routes like this. It can then be quite easy to skim our defined URL endpoints in one place.

router.get(
{
path: TRUSTED_APPS_LIST_API,
validate: GetTrustedAppsRequestSchema,
Copy link
Contributor

Choose a reason for hiding this comment

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

I noticed that all other schemas are just made inline instead of separate files. It seems to me to make sense - controller layer should only contain HTTP mapping related functionality hence it is already separate responsibility (separate concern) and splitting it further only creates more indirection, rather keeping path, schema and other things inline here gives a really good overview of API.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see multiple patterns across the app and Kibana. Defining it inline here makes it harder (impossible?) to test because currently we are not actually simulating an HTTP request to the Kibana server (not sure thats supported in the UT framework - ex. does kibana core provide a mock "server"?), but rather we test against the HTTP handler.

Also - as I learned from Frank, its best to ensure we test our schemas separately so that if we do end up changing schema libraries (ex. move to io-ts), we can ensure that our expectation remain met with the new lib.

Copy link
Member

Choose a reason for hiding this comment

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

One thing I did like about the everything-defined-here pattern was that the schema was close to the URL route. This adds two layers of separation. Need to look in constants to see what the route is, and handlers to see what the params are.

@paul-tavares
Copy link
Contributor Author

jenkins test this

1 similar comment
@paul-tavares
Copy link
Contributor Author

jenkins test this

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Build metrics

@kbn/optimizer bundle module count

id value diff baseline
securitySolution 1923 +1 1922

async chunks size

id value diff baseline
securitySolution 9.5MB +986.0B 9.5MB

page load bundle size

id value diff baseline
securitySolution 807.8KB +1.0B 807.8KB

distributable file count

id value diff baseline
total 53204 +7 53197

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@paul-tavares paul-tavares merged commit 9873df8 into elastic:master Aug 26, 2020
@paul-tavares paul-tavares deleted the task/EMT-685-trusted-apps-list-api branch August 26, 2020 20:02
paul-tavares added a commit to paul-tavares/kibana that referenced this pull request Aug 26, 2020
* Trusted Apps initial setup for route registration

* Added types for TrustedApp entries

* trusted apps list API returns results

* use methods and const from latest PR merge to lists

* a quick generator for trusted apps entries

* support cli options for trusted app data loader

* Add mocked `createTrustedAppsList()` method to `ExceptionListClientMock`

* tests fro trusted apps route handlers

* tests for trusted apps schema

* Correct name of mock method

* Fix service to ensure return value of `getExceptionList` service throws if service not available

* Fix types

* Refactor TrustedApp type + code review feedback
paul-tavares added a commit that referenced this pull request Aug 27, 2020
* Trusted Apps API for retrieving list
* Added types for TrustedApp entries
* generator for trusted apps entries
* Add mocked `createTrustedAppsList()` method to `ExceptionListClientMock`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature:Endpoint Elastic Endpoint feature release_note:skip Skip the PR/issue when compiling release notes v7.10.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants