Skip to content

Commit

Permalink
[SECURITY_SOLUTION][ENDPOINT] Trusted Apps List API (#75476)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
paul-tavares authored Aug 26, 2020
1 parent 532f2d7 commit 9873df8
Show file tree
Hide file tree
Showing 16 changed files with 503 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import {
_VERSION,
} from '../../constants.mock';
import { ENDPOINT_LIST_ID } from '../..';
import {
ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION,
ENDPOINT_TRUSTED_APPS_LIST_ID,
ENDPOINT_TRUSTED_APPS_LIST_NAME,
} from '../../constants';

import { ExceptionListSchema } from './exception_list_schema';

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

export const getTrustedAppsListSchemaMock = (): ExceptionListSchema => {
return {
...getExceptionListSchemaMock(),
description: ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION,
list_id: ENDPOINT_TRUSTED_APPS_LIST_ID,
name: ENDPOINT_TRUSTED_APPS_LIST_NAME,
};
};

/**
* This is useful for end to end tests where we remove the auto generated parts for comparisons
* such as created_at, updated_at, and id.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { savedObjectsClientMock } from 'src/core/server/mocks';
import { getFoundExceptionListSchemaMock } from '../../../common/schemas/response/found_exception_list_schema.mock';
import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock';
import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock';
import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock';
import {
getExceptionListSchemaMock,
getTrustedAppsListSchemaMock,
} from '../../../common/schemas/response/exception_list_schema.mock';

import { ExceptionListClient } from './exception_list_client';

Expand All @@ -24,6 +27,7 @@ export class ExceptionListClientMock extends ExceptionListClient {
public deleteExceptionListItem = jest.fn().mockResolvedValue(getExceptionListItemSchemaMock());
public findExceptionListItem = jest.fn().mockResolvedValue(getFoundExceptionListItemSchemaMock());
public findExceptionList = jest.fn().mockResolvedValue(getFoundExceptionListSchemaMock());
public createTrustedAppsList = jest.fn().mockResolvedValue(getTrustedAppsListSchemaMock());
}

export const getExceptionListClientMock = (): ExceptionListClient => {
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/security_solution/common/endpoint/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
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 })),
per_page: schema.maybe(schema.number({ defaultValue: 20, min: 1 })),
}),
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
*/

import { ApplicationStart } from 'kibana/public';
import { NewPackagePolicy, PackagePolicy } from '../../../ingest_manager/common';
import { ManifestSchema } from './schema/manifest';
import { NewPackagePolicy, PackagePolicy } from '../../../../ingest_manager/common';
import { ManifestSchema } from '../schema/manifest';

export * from './trusted_apps';

/**
* Supported React-Router state for the Policy Details page
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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';

/** API request params for retrieving a list of Trusted Apps */
export type GetTrustedAppsListRequest = TypeOf<typeof GetTrustedAppsRequestSchema.query>;
export interface GetTrustedListAppsResponse {
per_page: number;
page: number;
total: number;
data: TrustedApp[];
}

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

type WindowsConditionEntry =
| MacosLinuxConditionEntry
| (Omit<MacosLinuxConditionEntry, 'field'> & {
field: 'signer';
});

/** Type for a new Trusted App Entry */
export type NewTrustedApp = {
name: string;
description?: string;
} & (
| {
os: 'linux' | 'macos';
entries: MacosLinuxConditionEntry[];
}
| {
os: 'windows';
entries: WindowsConditionEntry[];
}
);

/** A trusted app entry */
export type TrustedApp = NewTrustedApp & {
id: string;
created_at: string;
created_by: 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 @@
/*
* 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
Expand Up @@ -12,10 +12,12 @@ import {
import { AgentService, IngestManagerStartContract } from '../../../ingest_manager/server';
import { getPackagePolicyCreateCallback } from './ingest_integration';
import { ManifestManager } from './services/artifacts';
import { ExceptionListClient } from '../../../lists/server';

export type EndpointAppContextServiceStartContract = Partial<
Pick<IngestManagerStartContract, 'agentService'>
> & {
exceptionsListService: ExceptionListClient;
logger: Logger;
manifestManager?: ManifestManager;
registerIngestCallback?: IngestManagerStartContract['registerExternalCallback'];
Expand All @@ -30,9 +32,11 @@ export class EndpointAppContextService {
private agentService: AgentService | undefined;
private manifestManager: ManifestManager | undefined;
private savedObjectsStart: SavedObjectsServiceStart | undefined;
private exceptionsListService: ExceptionListClient | undefined;

public start(dependencies: EndpointAppContextServiceStartContract) {
this.agentService = dependencies.agentService;
this.exceptionsListService = dependencies.exceptionsListService;
this.manifestManager = dependencies.manifestManager;
this.savedObjectsStart = dependencies.savedObjectsStart;

Expand All @@ -50,6 +54,13 @@ export class EndpointAppContextService {
return this.agentService;
}

public getExceptionsList() {
if (!this.exceptionsListService) {
throw new Error('exceptionsListService not set');
}
return this.exceptionsListService;
}

public getManifestManager(): ManifestManager | undefined {
return this.manifestManager;
}
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/security_solution/server/endpoint/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { ManifestManager } from './services/artifacts/manifest_manager/manifest_manager';
import { getManifestManagerMock } from './services/artifacts/manifest_manager/manifest_manager.mock';
import { EndpointAppContext } from './types';
import { listMock } from '../../../lists/server/mocks';

/**
* Creates a mocked EndpointAppContext.
Expand Down Expand Up @@ -58,6 +59,7 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked<
> => {
return {
agentService: createMockAgentService(),
exceptionsListService: listMock.getExceptionListClient(),
logger: loggingSystemMock.create().get('mock_endpoint_app_context'),
savedObjectsStart: savedObjectsServiceMock.createStartContract(),
manifestManager: getManifestManagerMock(),
Expand Down
Loading

0 comments on commit 9873df8

Please sign in to comment.