mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution][Lists] Add API level validation for Trusted Application via Lists Plugin extension points (#122454)
## Lists Plugin changes: - Modified ExceptionListClient to accept an optional KibanaRequest when instantiating a new instance of the class - Changes the extension points callback argument structure to an object having context and data. Context provides to the callbacks the HTTP request so that additional validation can be performed (ex. Authz to certain features) - ExtensionPointStorageClient#pipeRun() will now throw if an extension point callback also throws an error (instead of logging it and continuing on with callback execution) - ErrorWithStatusCode was export'ed out of the server (as ListsErrorWithStatusCode) and available for use by dependent plugins ## Security Solution Plugin (endpoint) changes: - Added new getEndpointAuthz(request) and getExceptionListsClient() methods to EndpointAppContextService - Added new server lists integration modules. Registers extension points with the Lists plugin for create and update of exception items. Currently validates only Trusted Apps - Added exception item artifact validators: - a BaseValidator with several generic and reusable methods that can be applied to any artifact - a TrustedAppValidator to specifically validate Trusted Applications - Refactor: - moved EndpointFleetServices to its own folder and also renamed it to include the word Factory (will help in the future if we create server-side service clients for working with Endpoint Policies) - Created common Artifact utilities and const's for working with ExceptionListItemSchema items
This commit is contained in:
parent
f4046b7f56
commit
a3181a5338
47 changed files with 1634 additions and 172 deletions
|
@ -23,12 +23,15 @@ export type {
|
|||
ListsServerExtensionRegistrar,
|
||||
ExtensionPoint,
|
||||
ExceptionsListPreCreateItemServerExtension,
|
||||
ExceptionListPreUpdateItemServerExtension,
|
||||
ExceptionsListPreUpdateItemServerExtension,
|
||||
} from './types';
|
||||
export type { ExportExceptionListAndItemsReturn } from './services/exception_lists/export_exception_list_and_items';
|
||||
|
||||
export const config: PluginConfigDescriptor = {
|
||||
schema: ConfigSchema,
|
||||
};
|
||||
|
||||
export { ErrorWithStatusCode as ListsErrorWithStatusCode } from './error_with_status_code';
|
||||
|
||||
export const plugin = (initializerContext: PluginInitializerContext): ListPlugin =>
|
||||
new ListPlugin(initializerContext);
|
||||
|
|
|
@ -7,7 +7,10 @@
|
|||
|
||||
import { ListPluginSetup } from './types';
|
||||
import { getListClientMock } from './services/lists/list_client.mock';
|
||||
import { getExceptionListClientMock } from './services/exception_lists/exception_list_client.mock';
|
||||
import {
|
||||
getCreateExceptionListItemOptionsMock,
|
||||
getExceptionListClientMock,
|
||||
} from './services/exception_lists/exception_list_client.mock';
|
||||
|
||||
const createSetupMock = (): jest.Mocked<ListPluginSetup> => {
|
||||
const mock: jest.Mocked<ListPluginSetup> = {
|
||||
|
@ -20,6 +23,7 @@ const createSetupMock = (): jest.Mocked<ListPluginSetup> => {
|
|||
|
||||
export const listMock = {
|
||||
createSetup: createSetupMock,
|
||||
getCreateExceptionListItemOptionsMock,
|
||||
getExceptionListClient: getExceptionListClientMock,
|
||||
getListClient: getListClientMock,
|
||||
};
|
||||
|
|
|
@ -114,6 +114,7 @@ export class ListPlugin implements Plugin<ListPluginSetup, ListsPluginStart, {},
|
|||
return {
|
||||
getExceptionListClient: (): ExceptionListClient =>
|
||||
new ExceptionListClient({
|
||||
request,
|
||||
savedObjectsClient,
|
||||
serverExtensionsClient: this.extensionPoints.getClient(),
|
||||
user,
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
ExtensionPointStorageContextMock,
|
||||
createExtensionPointStorageMock,
|
||||
} from '../extension_points/extension_point_storage.mock';
|
||||
import type { ExtensionPointCallbackArgument } from '../extension_points';
|
||||
import type { ExtensionPointCallbackDataArgument } from '../extension_points';
|
||||
|
||||
import {
|
||||
getCreateExceptionListItemOptionsMock,
|
||||
|
@ -112,10 +112,12 @@ describe('exception_list_client', () => {
|
|||
it('should validate extension point callback returned data and throw if not valid', async () => {
|
||||
const extensionPointCallback = getExtensionPointCallback();
|
||||
extensionPointCallback.mockImplementation(async (args) => {
|
||||
const { entries, ...rest } = args as ExtensionPointCallbackArgument;
|
||||
const {
|
||||
data: { entries, ...rest },
|
||||
} = args as { data: ExtensionPointCallbackDataArgument };
|
||||
|
||||
expect(entries).toBeTruthy(); // Test entries to exist since we exclude it.
|
||||
return rest as ExtensionPointCallbackArgument;
|
||||
return rest as ExtensionPointCallbackDataArgument;
|
||||
});
|
||||
|
||||
const methodResponsePromise = callExceptionListClientMethod();
|
||||
|
@ -126,6 +128,7 @@ describe('exception_list_client', () => {
|
|||
reason: ['Invalid value "undefined" supplied to "entries"'],
|
||||
})
|
||||
);
|
||||
expect(extensionPointStorageContext.logger.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use data returned from extension point callbacks when saving', async () => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObjectsClientContract } from 'kibana/server';
|
||||
import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server';
|
||||
import {
|
||||
ExceptionListItemSchema,
|
||||
ExceptionListSchema,
|
||||
|
@ -18,7 +18,10 @@ import {
|
|||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants';
|
||||
|
||||
import type { ExtensionPointStorageClientInterface } from '../extension_points';
|
||||
import type {
|
||||
ExtensionPointStorageClientInterface,
|
||||
ServerExtensionCallbackContext,
|
||||
} from '../extension_points';
|
||||
|
||||
import {
|
||||
ConstructorOptions,
|
||||
|
@ -81,17 +84,26 @@ export class ExceptionListClient {
|
|||
private readonly savedObjectsClient: SavedObjectsClientContract;
|
||||
private readonly serverExtensionsClient: ExtensionPointStorageClientInterface;
|
||||
private readonly enableServerExtensionPoints: boolean;
|
||||
private readonly request?: KibanaRequest;
|
||||
|
||||
constructor({
|
||||
user,
|
||||
savedObjectsClient,
|
||||
serverExtensionsClient,
|
||||
enableServerExtensionPoints = true,
|
||||
request,
|
||||
}: ConstructorOptions) {
|
||||
this.user = user;
|
||||
this.savedObjectsClient = savedObjectsClient;
|
||||
this.serverExtensionsClient = serverExtensionsClient;
|
||||
this.enableServerExtensionPoints = enableServerExtensionPoints;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
private getServerExtensionCallbackContext(): ServerExtensionCallbackContext {
|
||||
return {
|
||||
request: this.request,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -404,6 +416,7 @@ export class ExceptionListClient {
|
|||
itemData = await this.serverExtensionsClient.pipeRun(
|
||||
'exceptionsListPreCreateItem',
|
||||
itemData,
|
||||
this.getServerExtensionCallbackContext(),
|
||||
(data) => {
|
||||
return validateData(
|
||||
createExceptionListItemSchema,
|
||||
|
@ -470,6 +483,7 @@ export class ExceptionListClient {
|
|||
updatedItem = await this.serverExtensionsClient.pipeRun(
|
||||
'exceptionsListPreUpdateItem',
|
||||
updatedItem,
|
||||
this.getServerExtensionCallbackContext(),
|
||||
(data) => {
|
||||
return validateData(
|
||||
updateExceptionListItemSchema,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { SavedObjectsClientContract } from 'kibana/server';
|
||||
import type { KibanaRequest, SavedObjectsClientContract } from 'kibana/server';
|
||||
import type {
|
||||
CreateCommentsArray,
|
||||
Description,
|
||||
|
@ -58,6 +58,8 @@ export interface ConstructorOptions {
|
|||
serverExtensionsClient: ExtensionPointStorageClientInterface;
|
||||
/** Set to `false` if wanting to disable executing registered server extension points. Default is true. */
|
||||
enableServerExtensionPoints?: boolean;
|
||||
/** Should be provided when creating an instance from an HTTP request handler */
|
||||
request?: KibanaRequest;
|
||||
}
|
||||
|
||||
export interface GetExceptionListOptions {
|
||||
|
|
|
@ -7,11 +7,14 @@
|
|||
|
||||
import { MockedLogger, loggerMock } from '@kbn/logging/mocks';
|
||||
|
||||
import { httpServerMock } from '../../../../../../src/core/server/mocks';
|
||||
|
||||
import { ExtensionPointStorage } from './extension_point_storage';
|
||||
import {
|
||||
ExceptionListPreUpdateItemServerExtension,
|
||||
ExceptionsListPreCreateItemServerExtension,
|
||||
ExceptionsListPreUpdateItemServerExtension,
|
||||
ExtensionPointStorageInterface,
|
||||
ServerExtensionCallbackContext,
|
||||
} from './types';
|
||||
|
||||
export interface ExtensionPointStorageContextMock {
|
||||
|
@ -21,7 +24,8 @@ export interface ExtensionPointStorageContextMock {
|
|||
/** An Exception List Item pre-create extension point added to the storage. Appends `-1` to the data's `name` attribute */
|
||||
exceptionPreCreate: jest.Mocked<ExceptionsListPreCreateItemServerExtension>;
|
||||
/** An Exception List Item pre-update extension point added to the storage. Appends `-2` to the data's `name` attribute */
|
||||
exceptionPreUpdate: jest.Mocked<ExceptionListPreUpdateItemServerExtension>;
|
||||
exceptionPreUpdate: jest.Mocked<ExceptionsListPreUpdateItemServerExtension>;
|
||||
callbackContext: jest.Mocked<ServerExtensionCallbackContext>;
|
||||
}
|
||||
|
||||
export const createExtensionPointStorageMock = (
|
||||
|
@ -30,7 +34,7 @@ export const createExtensionPointStorageMock = (
|
|||
const extensionPointStorage = new ExtensionPointStorage(logger);
|
||||
|
||||
const exceptionPreCreate: ExtensionPointStorageContextMock['exceptionPreCreate'] = {
|
||||
callback: jest.fn(async (data) => {
|
||||
callback: jest.fn(async ({ data }) => {
|
||||
return {
|
||||
...data,
|
||||
name: `${data.name}-1`,
|
||||
|
@ -40,7 +44,7 @@ export const createExtensionPointStorageMock = (
|
|||
};
|
||||
|
||||
const exceptionPreUpdate: ExtensionPointStorageContextMock['exceptionPreUpdate'] = {
|
||||
callback: jest.fn(async (data) => {
|
||||
callback: jest.fn(async ({ data }) => {
|
||||
return {
|
||||
...data,
|
||||
name: `${data.name}-1`,
|
||||
|
@ -53,6 +57,9 @@ export const createExtensionPointStorageMock = (
|
|||
extensionPointStorage.add(exceptionPreUpdate);
|
||||
|
||||
return {
|
||||
callbackContext: {
|
||||
request: httpServerMock.createKibanaRequest(),
|
||||
},
|
||||
exceptionPreCreate,
|
||||
exceptionPreUpdate,
|
||||
extensionPointStorage,
|
||||
|
|
|
@ -13,11 +13,12 @@ import { DataValidationError } from '../exception_lists/utils/errors';
|
|||
|
||||
import { ExtensionPointError } from './errors';
|
||||
import {
|
||||
ExceptionListPreUpdateItemServerExtension,
|
||||
ExceptionsListPreCreateItemServerExtension,
|
||||
ExceptionsListPreUpdateItemServerExtension,
|
||||
ExtensionPoint,
|
||||
ExtensionPointStorageClientInterface,
|
||||
ExtensionPointStorageInterface,
|
||||
ServerExtensionCallbackContext,
|
||||
} from './types';
|
||||
import { createExtensionPointStorageMock } from './extension_point_storage.mock';
|
||||
|
||||
|
@ -25,6 +26,7 @@ describe('When using the ExtensionPointStorageClient', () => {
|
|||
let storageClient: ExtensionPointStorageClientInterface;
|
||||
let logger: ReturnType<typeof loggerMock.create>;
|
||||
let extensionPointStorage: ExtensionPointStorageInterface;
|
||||
let callbackContext: ServerExtensionCallbackContext;
|
||||
let preCreateExtensionPointMock1: jest.Mocked<ExceptionsListPreCreateItemServerExtension>;
|
||||
let extensionPointsMocks: Array<jest.Mocked<ExtensionPoint>>;
|
||||
let callbackRunLog: string;
|
||||
|
@ -34,10 +36,10 @@ describe('When using the ExtensionPointStorageClient', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const storageContext = createExtensionPointStorageMock();
|
||||
const extensionPointStorageMock = createExtensionPointStorageMock();
|
||||
|
||||
callbackRunLog = '';
|
||||
({ logger, extensionPointStorage } = storageContext);
|
||||
({ logger, extensionPointStorage, callbackContext } = extensionPointStorageMock);
|
||||
extensionPointStorage.clear();
|
||||
|
||||
// Generic callback function that also logs to the `callbackRunLog` its id, so we know the order in which they ran.
|
||||
|
@ -48,12 +50,12 @@ describe('When using the ExtensionPointStorageClient', () => {
|
|||
A extends Parameters<T['callback']>[0] = Parameters<T['callback']>[0]
|
||||
>(
|
||||
id: number,
|
||||
arg: A
|
||||
): Promise<A> => {
|
||||
{ data }: A
|
||||
): Promise<A['data']> => {
|
||||
callbackRunLog += id;
|
||||
return {
|
||||
...arg,
|
||||
name: `${arg.name}-${id}`,
|
||||
...data,
|
||||
name: `${data.name}-${id}`,
|
||||
};
|
||||
};
|
||||
preCreateExtensionPointMock1 = {
|
||||
|
@ -72,7 +74,7 @@ describe('When using the ExtensionPointStorageClient', () => {
|
|||
},
|
||||
{
|
||||
callback: jest.fn(
|
||||
callbackFn.bind(window, 3) as ExceptionListPreUpdateItemServerExtension['callback']
|
||||
callbackFn.bind(window, 3) as ExceptionsListPreUpdateItemServerExtension['callback']
|
||||
),
|
||||
type: 'exceptionsListPreUpdateItem',
|
||||
},
|
||||
|
@ -115,38 +117,70 @@ describe('When using the ExtensionPointStorageClient', () => {
|
|||
it('should run extension point callbacks serially', async () => {
|
||||
await storageClient.pipeRun(
|
||||
'exceptionsListPreCreateItem',
|
||||
createExceptionListItemOptionsMock
|
||||
createExceptionListItemOptionsMock,
|
||||
callbackContext
|
||||
);
|
||||
expect(callbackRunLog).toEqual('1245');
|
||||
});
|
||||
|
||||
it('should provide `context` to every callback', async () => {
|
||||
await storageClient.pipeRun(
|
||||
'exceptionsListPreCreateItem',
|
||||
createExceptionListItemOptionsMock,
|
||||
callbackContext
|
||||
);
|
||||
for (const extensionPointsMock of extensionPointsMocks) {
|
||||
if (extensionPointsMock.type === 'exceptionsListPreCreateItem') {
|
||||
expect(extensionPointsMock.callback).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
context: {
|
||||
request: expect.any(Object),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should pass the return value of one extensionPoint to the next', async () => {
|
||||
await storageClient.pipeRun(
|
||||
'exceptionsListPreCreateItem',
|
||||
createExceptionListItemOptionsMock
|
||||
createExceptionListItemOptionsMock,
|
||||
callbackContext
|
||||
);
|
||||
|
||||
expect(extensionPointsMocks[0].callback).toHaveBeenCalledWith(
|
||||
createExceptionListItemOptionsMock
|
||||
);
|
||||
expect(extensionPointsMocks[0].callback).toHaveBeenCalledWith({
|
||||
context: callbackContext,
|
||||
data: createExceptionListItemOptionsMock,
|
||||
});
|
||||
expect(extensionPointsMocks[1].callback).toHaveBeenCalledWith({
|
||||
...createExceptionListItemOptionsMock,
|
||||
name: `${createExceptionListItemOptionsMock.name}-1`,
|
||||
context: callbackContext,
|
||||
data: {
|
||||
...createExceptionListItemOptionsMock,
|
||||
name: `${createExceptionListItemOptionsMock.name}-1`,
|
||||
},
|
||||
});
|
||||
expect(extensionPointsMocks[3].callback).toHaveBeenCalledWith({
|
||||
...createExceptionListItemOptionsMock,
|
||||
name: `${createExceptionListItemOptionsMock.name}-1-2`,
|
||||
context: callbackContext,
|
||||
data: {
|
||||
...createExceptionListItemOptionsMock,
|
||||
name: `${createExceptionListItemOptionsMock.name}-1-2`,
|
||||
},
|
||||
});
|
||||
expect(extensionPointsMocks[4].callback).toHaveBeenCalledWith({
|
||||
...createExceptionListItemOptionsMock,
|
||||
name: `${createExceptionListItemOptionsMock.name}-1-2-4`,
|
||||
context: callbackContext,
|
||||
data: {
|
||||
...createExceptionListItemOptionsMock,
|
||||
name: `${createExceptionListItemOptionsMock.name}-1-2-4`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a data structure similar to the one provided initially', async () => {
|
||||
const result = await storageClient.pipeRun(
|
||||
'exceptionsListPreCreateItem',
|
||||
createExceptionListItemOptionsMock
|
||||
createExceptionListItemOptionsMock,
|
||||
callbackContext
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
|
@ -155,36 +189,20 @@ describe('When using the ExtensionPointStorageClient', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should log an error if extension point callback Throw's", async () => {
|
||||
it('should stop execution of other extension points after encountering one that `throw`s', async () => {
|
||||
const extensionError = new Error('foo');
|
||||
preCreateExtensionPointMock1.callback.mockImplementation(async () => {
|
||||
throw extensionError;
|
||||
});
|
||||
|
||||
await storageClient.pipeRun(
|
||||
const resultPromise = storageClient.pipeRun(
|
||||
'exceptionsListPreCreateItem',
|
||||
createExceptionListItemOptionsMock
|
||||
createExceptionListItemOptionsMock,
|
||||
callbackContext
|
||||
);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(expect.any(ExtensionPointError));
|
||||
expect(logger.error.mock.calls[0][0]).toMatchObject({ meta: extensionError });
|
||||
});
|
||||
|
||||
it('should continue to other extension points after encountering one that `throw`s', async () => {
|
||||
const extensionError = new Error('foo');
|
||||
preCreateExtensionPointMock1.callback.mockImplementation(async () => {
|
||||
throw extensionError;
|
||||
});
|
||||
|
||||
const result = await storageClient.pipeRun(
|
||||
'exceptionsListPreCreateItem',
|
||||
createExceptionListItemOptionsMock
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
...createExceptionListItemOptionsMock,
|
||||
name: `${createExceptionListItemOptionsMock.name}-2-4-5`,
|
||||
});
|
||||
await expect(resultPromise).rejects.toBe(extensionError);
|
||||
expect(extensionPointsMocks[1].callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log an error and Throw if external callback returned invalid data', async () => {
|
||||
|
@ -194,6 +212,7 @@ describe('When using the ExtensionPointStorageClient', () => {
|
|||
storageClient.pipeRun(
|
||||
'exceptionsListPreCreateItem',
|
||||
createExceptionListItemOptionsMock,
|
||||
callbackContext,
|
||||
() => {
|
||||
return validationError;
|
||||
}
|
||||
|
|
|
@ -9,10 +9,11 @@ import { Logger } from 'kibana/server';
|
|||
|
||||
import type {
|
||||
ExtensionPoint,
|
||||
ExtensionPointCallbackArgument,
|
||||
ExtensionPointCallbackDataArgument,
|
||||
ExtensionPointStorageClientInterface,
|
||||
ExtensionPointStorageInterface,
|
||||
NarrowExtensionPointToType,
|
||||
ServerExtensionCallbackContext,
|
||||
} from './types';
|
||||
import { ExtensionPointError } from './errors';
|
||||
|
||||
|
@ -38,6 +39,7 @@ export class ExtensionPointStorageClient implements ExtensionPointStorageClientI
|
|||
*
|
||||
* @param extensionType
|
||||
* @param initialCallbackInput The initial argument given to the first extension point callback
|
||||
* @param callbackContext
|
||||
* @param callbackResponseValidator A function to validate the returned data from an extension point callback
|
||||
*/
|
||||
async pipeRun<
|
||||
|
@ -46,9 +48,10 @@ export class ExtensionPointStorageClient implements ExtensionPointStorageClientI
|
|||
P extends Parameters<D['callback']> = Parameters<D['callback']>
|
||||
>(
|
||||
extensionType: T,
|
||||
initialCallbackInput: P[0],
|
||||
callbackResponseValidator?: (data: P[0]) => Error | undefined
|
||||
): Promise<P[0]> {
|
||||
initialCallbackInput: P[0]['data'],
|
||||
callbackContext: ServerExtensionCallbackContext,
|
||||
callbackResponseValidator?: (data: P[0]['data']) => Error | undefined
|
||||
): Promise<P[0]['data']> {
|
||||
let inputArgument = initialCallbackInput;
|
||||
const externalExtensions = this.get(extensionType);
|
||||
|
||||
|
@ -60,26 +63,17 @@ export class ExtensionPointStorageClient implements ExtensionPointStorageClientI
|
|||
const extensionRegistrationSource =
|
||||
this.storage.getExtensionRegistrationSource(externalExtension);
|
||||
|
||||
try {
|
||||
inputArgument = await externalExtension.callback(
|
||||
inputArgument as ExtensionPointCallbackArgument
|
||||
);
|
||||
} catch (error) {
|
||||
// Log the error that the external callback threw and keep going with the running of others
|
||||
this.logger?.error(
|
||||
new ExtensionPointError(
|
||||
`Extension point execution error for ${externalExtension.type}: ${extensionRegistrationSource}`,
|
||||
error
|
||||
)
|
||||
);
|
||||
}
|
||||
inputArgument = await externalExtension.callback({
|
||||
context: callbackContext,
|
||||
data: inputArgument as ExtensionPointCallbackDataArgument,
|
||||
});
|
||||
|
||||
if (callbackResponseValidator) {
|
||||
// Before calling the next one, make sure the returned payload is valid
|
||||
const validationError = callbackResponseValidator(inputArgument);
|
||||
|
||||
if (validationError) {
|
||||
this.logger?.error(
|
||||
this.logger.error(
|
||||
new ExtensionPointError(
|
||||
`Extension point for ${externalExtension.type} returned data that failed validation: ${extensionRegistrationSource}`,
|
||||
{
|
||||
|
|
|
@ -5,24 +5,51 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PromiseType } from 'utility-types';
|
||||
import { UnionToIntersection } from '@kbn/utility-types';
|
||||
import { KibanaRequest } from 'kibana/server';
|
||||
|
||||
import {
|
||||
CreateExceptionListItemOptions,
|
||||
UpdateExceptionListItemOptions,
|
||||
} from '../exception_lists/exception_list_client_types';
|
||||
|
||||
export type ServerExtensionCallback<A extends object | void = void, R = unknown> = (
|
||||
args: A
|
||||
) => Promise<R>;
|
||||
/**
|
||||
* The `this` context provided to extension point's callback function
|
||||
* NOTE: in order to access this context, callbacks **MUST** be defined using `function()` instead of arrow functions.
|
||||
*/
|
||||
export interface ServerExtensionCallbackContext {
|
||||
/**
|
||||
* The Lists plugin HTTP Request. May be undefined if the callback is executed from a area of code that
|
||||
* is not triggered via one of the HTTP handlers
|
||||
*/
|
||||
request?: KibanaRequest;
|
||||
}
|
||||
|
||||
export type ServerExtensionCallback<A extends object | void = void, R = A> = (args: {
|
||||
context: ServerExtensionCallbackContext;
|
||||
data: A;
|
||||
}) => Promise<R>;
|
||||
|
||||
interface ServerExtensionPointDefinition<
|
||||
T extends string,
|
||||
Args extends object | void = void,
|
||||
Response = void
|
||||
Response = Args
|
||||
> {
|
||||
type: T;
|
||||
/**
|
||||
* The callback that will be executed at the given extension point. The Function will be provided a context (`this)`
|
||||
* that includes supplemental data associated with its type. In order to access that data, the callback **MUST**
|
||||
* be defined using `function()` and NOT an arrow function.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* {
|
||||
* type: 'some type',
|
||||
* callback: function() {
|
||||
* // this === context is available
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
callback: ServerExtensionCallback<Args, Response>;
|
||||
}
|
||||
|
||||
|
@ -32,7 +59,6 @@ interface ServerExtensionPointDefinition<
|
|||
*/
|
||||
export type ExceptionsListPreCreateItemServerExtension = ServerExtensionPointDefinition<
|
||||
'exceptionsListPreCreateItem',
|
||||
CreateExceptionListItemOptions,
|
||||
CreateExceptionListItemOptions
|
||||
>;
|
||||
|
||||
|
@ -40,15 +66,14 @@ export type ExceptionsListPreCreateItemServerExtension = ServerExtensionPointDef
|
|||
* Extension point is triggered prior to updating the Exception List Item. Throw'ing will cause the
|
||||
* update operation to fail
|
||||
*/
|
||||
export type ExceptionListPreUpdateItemServerExtension = ServerExtensionPointDefinition<
|
||||
export type ExceptionsListPreUpdateItemServerExtension = ServerExtensionPointDefinition<
|
||||
'exceptionsListPreUpdateItem',
|
||||
UpdateExceptionListItemOptions,
|
||||
UpdateExceptionListItemOptions
|
||||
>;
|
||||
|
||||
export type ExtensionPoint =
|
||||
| ExceptionsListPreCreateItemServerExtension
|
||||
| ExceptionListPreUpdateItemServerExtension;
|
||||
| ExceptionsListPreUpdateItemServerExtension;
|
||||
|
||||
/**
|
||||
* A Map of extension point type and associated Set of callbacks
|
||||
|
@ -57,6 +82,7 @@ export type ExtensionPoint =
|
|||
* Registration function for server-side extension points
|
||||
*/
|
||||
export type ListsServerExtensionRegistrar = (extension: ExtensionPoint) => void;
|
||||
|
||||
export type NarrowExtensionPointToType<T extends ExtensionPoint['type']> = {
|
||||
type: T;
|
||||
} & ExtensionPoint;
|
||||
|
@ -65,8 +91,8 @@ export type NarrowExtensionPointToType<T extends ExtensionPoint['type']> = {
|
|||
* An intersection of all callback arguments for use internally when
|
||||
* casting (ex. in `ExtensionPointStorageClient#pipeRun()`
|
||||
*/
|
||||
export type ExtensionPointCallbackArgument = UnionToIntersection<
|
||||
PromiseType<ReturnType<ExtensionPoint['callback']>>
|
||||
export type ExtensionPointCallbackDataArgument = UnionToIntersection<
|
||||
Parameters<ExtensionPoint['callback']>[0]['data']
|
||||
>;
|
||||
|
||||
export interface ExtensionPointStorageClientInterface {
|
||||
|
@ -80,9 +106,10 @@ export interface ExtensionPointStorageClientInterface {
|
|||
P extends Parameters<D['callback']> = Parameters<D['callback']>
|
||||
>(
|
||||
extensionType: T,
|
||||
initialCallbackInput: P[0],
|
||||
callbackResponseValidator?: (data: P[0]) => Error | undefined
|
||||
): Promise<P[0]>;
|
||||
initialCallbackInput: P[0]['data'],
|
||||
callbackContext: ServerExtensionCallbackContext,
|
||||
callbackResponseValidator?: (data: P[0]['data']) => Error | undefined
|
||||
): Promise<P[0]['data']>;
|
||||
}
|
||||
|
||||
export interface ExtensionPointStorageInterface {
|
||||
|
|
|
@ -75,7 +75,7 @@ export type ContextProviderReturn = Promise<ListsApiRequestHandlerContext>;
|
|||
|
||||
export type {
|
||||
ExtensionPoint,
|
||||
ExceptionListPreUpdateItemServerExtension,
|
||||
ExceptionsListPreUpdateItemServerExtension,
|
||||
ExceptionsListPreCreateItemServerExtension,
|
||||
ListsServerExtensionRegistrar,
|
||||
} from './services/extension_points';
|
||||
|
|
|
@ -5,10 +5,34 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import type {
|
||||
ExceptionListItemSchema,
|
||||
CreateExceptionListItemSchema,
|
||||
UpdateExceptionListItemSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants';
|
||||
import { BaseDataGenerator } from './base_data_generator';
|
||||
import { POLICY_REFERENCE_PREFIX } from '../service/trusted_apps/mapping';
|
||||
import { ConditionEntryField } from '../types';
|
||||
import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../service/artifacts/constants';
|
||||
|
||||
/** Utility that removes null and undefined from a Type's property value */
|
||||
type NonNullableTypeProperties<T> = {
|
||||
[P in keyof T]-?: NonNullable<T[P]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalizes the create type to remove `undefined`/`null` from the returned type since the generator or sure to
|
||||
* create a value for (almost) all properties
|
||||
*/
|
||||
type CreateExceptionListItemSchemaWithNonNullProps = NonNullableTypeProperties<
|
||||
Omit<CreateExceptionListItemSchema, 'meta'>
|
||||
> &
|
||||
Pick<CreateExceptionListItemSchema, 'meta'>;
|
||||
|
||||
type UpdateExceptionListItemSchemaWithNonNullProps = NonNullableTypeProperties<
|
||||
Omit<UpdateExceptionListItemSchema, 'meta'>
|
||||
> &
|
||||
Pick<UpdateExceptionListItemSchema, 'meta'>;
|
||||
|
||||
export class ExceptionsListItemGenerator extends BaseDataGenerator<ExceptionListItemSchema> {
|
||||
generate(overrides: Partial<ExceptionListItemSchema> = {}): ExceptionListItemSchema {
|
||||
|
@ -38,11 +62,11 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator<ExceptionList
|
|||
id: this.seededUUIDv4(),
|
||||
item_id: this.seededUUIDv4(),
|
||||
list_id: 'endpoint_list_id',
|
||||
meta: {},
|
||||
meta: undefined,
|
||||
name: `Generated Exception (${this.randomString(5)})`,
|
||||
namespace_type: 'agnostic',
|
||||
os_types: [this.randomOSFamily()] as ExceptionListItemSchema['os_types'],
|
||||
tags: [`${POLICY_REFERENCE_PREFIX}all`],
|
||||
tags: [`${BY_POLICY_ARTIFACT_TAG_PREFIX}all`],
|
||||
tie_breaker_id: this.seededUUIDv4(),
|
||||
type: 'simple',
|
||||
updated_at: '2020-04-20T15:25:31.830Z',
|
||||
|
@ -50,4 +74,124 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator<ExceptionList
|
|||
...(overrides || {}),
|
||||
};
|
||||
}
|
||||
|
||||
generateForCreate(
|
||||
overrides: Partial<CreateExceptionListItemSchema> = {}
|
||||
): CreateExceptionListItemSchemaWithNonNullProps {
|
||||
const {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
description,
|
||||
entries,
|
||||
list_id,
|
||||
name,
|
||||
type,
|
||||
comments,
|
||||
item_id,
|
||||
meta,
|
||||
namespace_type,
|
||||
os_types,
|
||||
tags,
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
} = this.generate();
|
||||
|
||||
return {
|
||||
description,
|
||||
entries,
|
||||
list_id,
|
||||
name,
|
||||
type,
|
||||
comments,
|
||||
item_id,
|
||||
meta,
|
||||
namespace_type,
|
||||
os_types,
|
||||
tags,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
generateTrustedApp(overrides: Partial<ExceptionListItemSchema> = {}): ExceptionListItemSchema {
|
||||
const trustedApp = this.generate(overrides);
|
||||
|
||||
return {
|
||||
...trustedApp,
|
||||
name: `Trusted app (${this.randomString(5)})`,
|
||||
list_id: ENDPOINT_TRUSTED_APPS_LIST_ID,
|
||||
// Remove the hash field which the generator above currently still sets to a field that is not
|
||||
// actually valid when used with the Exception List
|
||||
entries: trustedApp.entries.filter((entry) => entry.field !== ConditionEntryField.HASH),
|
||||
};
|
||||
}
|
||||
|
||||
generateTrustedAppForCreate(
|
||||
overrides: Partial<CreateExceptionListItemSchema> = {}
|
||||
): CreateExceptionListItemSchemaWithNonNullProps {
|
||||
const {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
description,
|
||||
entries,
|
||||
list_id,
|
||||
name,
|
||||
type,
|
||||
comments,
|
||||
item_id,
|
||||
meta,
|
||||
namespace_type,
|
||||
os_types,
|
||||
tags,
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
} = this.generateTrustedApp();
|
||||
|
||||
return {
|
||||
description,
|
||||
entries,
|
||||
list_id,
|
||||
name,
|
||||
type,
|
||||
comments,
|
||||
item_id,
|
||||
meta,
|
||||
namespace_type,
|
||||
os_types,
|
||||
tags,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
generateTrustedAppForUpdate(
|
||||
overrides: Partial<UpdateExceptionListItemSchema> = {}
|
||||
): UpdateExceptionListItemSchemaWithNonNullProps {
|
||||
const {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
description,
|
||||
entries,
|
||||
name,
|
||||
type,
|
||||
comments,
|
||||
id,
|
||||
item_id,
|
||||
meta,
|
||||
namespace_type,
|
||||
os_types,
|
||||
tags,
|
||||
_version,
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
} = this.generateTrustedApp();
|
||||
|
||||
return {
|
||||
description,
|
||||
entries,
|
||||
name,
|
||||
type,
|
||||
comments,
|
||||
id,
|
||||
item_id,
|
||||
meta,
|
||||
namespace_type,
|
||||
os_types,
|
||||
tags,
|
||||
_version: _version ?? 'some value',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const BY_POLICY_ARTIFACT_TAG_PREFIX = 'policy:';
|
||||
|
||||
export const GLOBAL_ARTIFACT_TAG = `${BY_POLICY_ARTIFACT_TAG_PREFIX}all`;
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { isArtifactGlobal, isArtifactByPolicy, getPolicyIdsFromArtifact } from './utils';
|
||||
|
||||
export { BY_POLICY_ARTIFACT_TAG_PREFIX, GLOBAL_ARTIFACT_TAG } from './constants';
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { BY_POLICY_ARTIFACT_TAG_PREFIX, GLOBAL_ARTIFACT_TAG } from './constants';
|
||||
|
||||
const POLICY_ID_START_POSITION = BY_POLICY_ARTIFACT_TAG_PREFIX.length;
|
||||
|
||||
export const isArtifactGlobal = (item: Pick<ExceptionListItemSchema, 'tags'>): boolean => {
|
||||
return (item.tags ?? []).find((tag) => tag === GLOBAL_ARTIFACT_TAG) !== undefined;
|
||||
};
|
||||
|
||||
export const isArtifactByPolicy = (item: Pick<ExceptionListItemSchema, 'tags'>): boolean => {
|
||||
return !isArtifactGlobal(item);
|
||||
};
|
||||
|
||||
export const getPolicyIdsFromArtifact = (item: Pick<ExceptionListItemSchema, 'tags'>): string[] => {
|
||||
const policyIds = [];
|
||||
const tags = item.tags ?? [];
|
||||
|
||||
for (const tag of tags) {
|
||||
if (tag !== GLOBAL_ARTIFACT_TAG && tag.startsWith(BY_POLICY_ARTIFACT_TAG_PREFIX)) {
|
||||
policyIds.push(tag.substring(POLICY_ID_START_POSITION));
|
||||
}
|
||||
}
|
||||
|
||||
return policyIds;
|
||||
};
|
|
@ -6,8 +6,7 @@
|
|||
*/
|
||||
|
||||
import { EffectScope } from '../../types';
|
||||
|
||||
export const POLICY_REFERENCE_PREFIX = 'policy:';
|
||||
import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../artifacts/constants';
|
||||
|
||||
/**
|
||||
* Looks at an array of `tags` (attributed defined on the `ExceptionListItemSchema`) and returns back
|
||||
|
@ -15,16 +14,16 @@ export const POLICY_REFERENCE_PREFIX = 'policy:';
|
|||
* @param tags
|
||||
*/
|
||||
export const tagsToEffectScope = (tags: string[]): EffectScope => {
|
||||
const policyReferenceTags = tags.filter((tag) => tag.startsWith(POLICY_REFERENCE_PREFIX));
|
||||
const policyReferenceTags = tags.filter((tag) => tag.startsWith(BY_POLICY_ARTIFACT_TAG_PREFIX));
|
||||
|
||||
if (policyReferenceTags.some((tag) => tag === `${POLICY_REFERENCE_PREFIX}all`)) {
|
||||
if (policyReferenceTags.some((tag) => tag === `${BY_POLICY_ARTIFACT_TAG_PREFIX}all`)) {
|
||||
return {
|
||||
type: 'global',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'policy',
|
||||
policies: policyReferenceTags.map((tag) => tag.substr(POLICY_REFERENCE_PREFIX.length)),
|
||||
policies: policyReferenceTags.map((tag) => tag.substr(BY_POLICY_ARTIFACT_TAG_PREFIX.length)),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -26,7 +26,11 @@ export const getDuplicateFields = (entries: ConditionEntry[]) => {
|
|||
const groupedFields = new Map<ConditionEntryField, ConditionEntry[]>();
|
||||
|
||||
entries.forEach((entry) => {
|
||||
groupedFields.set(entry.field, [...(groupedFields.get(entry.field) || []), entry]);
|
||||
// With the move to the Exception Lists api, the server side now validates individual
|
||||
// `process.hash.[type]`'s, so we need to account for that here
|
||||
const field = entry.field.startsWith('process.hash') ? ConditionEntryField.HASH : entry.field;
|
||||
|
||||
groupedFields.set(field, [...(groupedFields.get(field) || []), entry]);
|
||||
});
|
||||
|
||||
return [...groupedFields.entries()]
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
|
||||
import { PolicyData } from '../../../../common/endpoint/types';
|
||||
import { EffectedPolicySelection } from './effected_policy_select';
|
||||
|
||||
export const GLOBAL_POLICY_TAG = 'policy:all';
|
||||
import { GLOBAL_ARTIFACT_TAG } from '../../../../common/endpoint/service/artifacts/constants';
|
||||
|
||||
/**
|
||||
* Given a list of artifact tags, returns the tags that are not policy tags
|
||||
|
@ -27,7 +26,7 @@ export function getArtifactTagsByEffectedPolicySelection(
|
|||
otherTags: string[] = []
|
||||
): string[] {
|
||||
if (selection.isGlobal) {
|
||||
return [GLOBAL_POLICY_TAG, ...otherTags];
|
||||
return [GLOBAL_ARTIFACT_TAG, ...otherTags];
|
||||
}
|
||||
const newTags = selection.selected.map((policy) => {
|
||||
return `policy:${policy.id}`;
|
||||
|
@ -47,7 +46,7 @@ export function getEffectedPolicySelectionByTags(
|
|||
tags: string[],
|
||||
policies: PolicyData[]
|
||||
): EffectedPolicySelection {
|
||||
if (tags.find((tag) => tag === GLOBAL_POLICY_TAG)) {
|
||||
if (tags.find((tag) => tag === GLOBAL_ARTIFACT_TAG)) {
|
||||
return {
|
||||
isGlobal: true,
|
||||
selected: [],
|
||||
|
@ -71,7 +70,7 @@ export function getEffectedPolicySelectionByTags(
|
|||
}
|
||||
|
||||
export function isGlobalPolicyEffected(tags?: string[]): boolean {
|
||||
return tags !== undefined && tags.find((tag) => tag === GLOBAL_POLICY_TAG) !== undefined;
|
||||
return tags !== undefined && tags.find((tag) => tag === GLOBAL_ARTIFACT_TAG) !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -26,7 +26,6 @@ import {
|
|||
ResponseProvidersInterface,
|
||||
} from '../../../common/mock/endpoint/http_handler_mock_factory';
|
||||
import { ExceptionsListItemGenerator } from '../../../../common/endpoint/data_generators/exceptions_list_item_generator';
|
||||
import { POLICY_REFERENCE_PREFIX } from '../../../../common/endpoint/service/trusted_apps/mapping';
|
||||
import { getTrustedAppsListSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_schema.mock';
|
||||
import {
|
||||
fleetGetAgentPolicyListHttpMock,
|
||||
|
@ -34,6 +33,7 @@ import {
|
|||
fleetGetEndpointPackagePolicyListHttpMock,
|
||||
FleetGetEndpointPackagePolicyListHttpMockInterface,
|
||||
} from './fleet_mocks';
|
||||
import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../../common/endpoint/service/artifacts/constants';
|
||||
|
||||
interface FindExceptionListItemSchemaQueryParams
|
||||
extends Omit<FindExceptionListItemSchema, 'page' | 'per_page'> {
|
||||
|
@ -67,8 +67,8 @@ export const trustedAppsGetListHttpMocks =
|
|||
data[2].tags = [
|
||||
// IDs below are those generated by the `fleetGetEndpointPackagePolicyListHttpMock()` mock,
|
||||
// so if using in combination with that API mock, these should just "work"
|
||||
`${POLICY_REFERENCE_PREFIX}ddf6570b-9175-4a6d-b288-61a09771c647`,
|
||||
`${POLICY_REFERENCE_PREFIX}b8e616ae-44fc-4be7-846c-ce8fa5c082dd`,
|
||||
`${BY_POLICY_ARTIFACT_TAG_PREFIX}ddf6570b-9175-4a6d-b288-61a09771c647`,
|
||||
`${BY_POLICY_ARTIFACT_TAG_PREFIX}b8e616ae-44fc-4be7-846c-ce8fa5c082dd`,
|
||||
];
|
||||
|
||||
return {
|
||||
|
|
|
@ -27,10 +27,8 @@ import {
|
|||
TrustedAppEntryTypes,
|
||||
UpdateTrustedApp,
|
||||
} from '../../../../../common/endpoint/types';
|
||||
import {
|
||||
POLICY_REFERENCE_PREFIX,
|
||||
tagsToEffectScope,
|
||||
} from '../../../../../common/endpoint/service/trusted_apps/mapping';
|
||||
import { tagsToEffectScope } from '../../../../../common/endpoint/service/trusted_apps/mapping';
|
||||
import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../../../common/endpoint/service/artifacts/constants';
|
||||
|
||||
type ConditionEntriesMap = { [K in ConditionEntryField]?: ConditionEntry<K> };
|
||||
type Mapping<T extends string, U> = { [K in T]: U };
|
||||
|
@ -177,9 +175,9 @@ const createEntryNested = (field: string, entries: NestedEntriesArray): EntryNes
|
|||
|
||||
const effectScopeToTags = (effectScope: EffectScope) => {
|
||||
if (effectScope.type === 'policy') {
|
||||
return effectScope.policies.map((policy) => `${POLICY_REFERENCE_PREFIX}${policy}`);
|
||||
return effectScope.policies.map((policy) => `${BY_POLICY_ARTIFACT_TAG_PREFIX}${policy}`);
|
||||
} else {
|
||||
return [`${POLICY_REFERENCE_PREFIX}all`];
|
||||
return [`${BY_POLICY_ARTIFACT_TAG_PREFIX}all`];
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -301,7 +301,6 @@ describe('When on the Trusted Apps Page', () => {
|
|||
id: '05b5e350-0cad-4dc3-a61d-6e6796b0af39',
|
||||
comments: [],
|
||||
item_id: '2d95bec3-b48f-4db7-9622-a2b061cc031d',
|
||||
meta: {},
|
||||
namespace_type: 'agnostic',
|
||||
type: 'simple',
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { KibanaRequest, Logger } from 'src/core/server';
|
||||
import { CreateExceptionListItemOptions, ExceptionListClient } from '../../../lists/server';
|
||||
import { ExceptionListClient } from '../../../lists/server';
|
||||
import {
|
||||
CasesClient,
|
||||
PluginStartContract as CasesPluginStartContract,
|
||||
|
@ -35,11 +35,14 @@ import {
|
|||
EndpointAppContentServicesNotStartedError,
|
||||
} from './errors';
|
||||
import {
|
||||
EndpointFleetServicesFactory,
|
||||
EndpointFleetServicesFactoryInterface,
|
||||
EndpointInternalFleetServicesInterface,
|
||||
EndpointScopedFleetServicesInterface,
|
||||
} from './services/endpoint_fleet_services';
|
||||
} from './services/fleet/endpoint_fleet_services_factory';
|
||||
import type { ListsServerExtensionRegistrar } from '../../../lists/server';
|
||||
import { registerListsPluginEndpointExtensionPoints } from '../lists_integration';
|
||||
import { EndpointAuthz } from '../../common/endpoint/types/authz';
|
||||
import { calculateEndpointAuthz } from '../../common/endpoint/service/authz';
|
||||
|
||||
export interface EndpointAppContextServiceSetupContract {
|
||||
securitySolutionRequestContextFactory: IRequestContextFactory;
|
||||
|
@ -51,8 +54,10 @@ export type EndpointAppContextServiceStartContract = Partial<
|
|||
'agentService' | 'packageService' | 'packagePolicyService' | 'agentPolicyService'
|
||||
>
|
||||
> & {
|
||||
fleetAuthzService?: FleetStartContract['authz'];
|
||||
logger: Logger;
|
||||
endpointMetadataService: EndpointMetadataService;
|
||||
endpointFleetServicesFactory: EndpointFleetServicesFactoryInterface;
|
||||
manifestManager?: ManifestManager;
|
||||
security: SecurityPluginStart;
|
||||
alerting: AlertsPluginStartContract;
|
||||
|
@ -71,7 +76,7 @@ export type EndpointAppContextServiceStartContract = Partial<
|
|||
export class EndpointAppContextService {
|
||||
private setupDependencies: EndpointAppContextServiceSetupContract | null = null;
|
||||
private startDependencies: EndpointAppContextServiceStartContract | null = null;
|
||||
private fleetServicesFactory: EndpointFleetServicesFactory | null = null;
|
||||
private fleetServicesFactory: EndpointFleetServicesFactoryInterface | null = null;
|
||||
public security: SecurityPluginStart | undefined;
|
||||
|
||||
public setup(dependencies: EndpointAppContextServiceSetupContract) {
|
||||
|
@ -85,17 +90,7 @@ export class EndpointAppContextService {
|
|||
|
||||
this.startDependencies = dependencies;
|
||||
this.security = dependencies.security;
|
||||
|
||||
// let's try to avoid turning off eslint's Forbidden non-null assertion rule
|
||||
const { agentService, agentPolicyService, packagePolicyService, packageService } =
|
||||
dependencies as Required<EndpointAppContextServiceStartContract>;
|
||||
|
||||
this.fleetServicesFactory = new EndpointFleetServicesFactory({
|
||||
agentService,
|
||||
agentPolicyService,
|
||||
packagePolicyService,
|
||||
packageService,
|
||||
});
|
||||
this.fleetServicesFactory = dependencies.endpointFleetServicesFactory;
|
||||
|
||||
if (dependencies.registerIngestCallback && dependencies.manifestManager) {
|
||||
dependencies.registerIngestCallback(
|
||||
|
@ -124,13 +119,7 @@ export class EndpointAppContextService {
|
|||
if (this.startDependencies.registerListsServerExtension) {
|
||||
const { registerListsServerExtension } = this.startDependencies;
|
||||
|
||||
registerListsServerExtension({
|
||||
type: 'exceptionsListPreCreateItem',
|
||||
callback: async (arg: CreateExceptionListItemOptions) => {
|
||||
// this.startDependencies?.logger.info('exceptionsListPreCreateItem called!');
|
||||
return arg;
|
||||
},
|
||||
});
|
||||
registerListsPluginEndpointExtensionPoints(registerListsServerExtension, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,6 +129,19 @@ export class EndpointAppContextService {
|
|||
return this.startDependencies?.config.experimentalFeatures;
|
||||
}
|
||||
|
||||
private getFleetAuthzService(): FleetStartContract['authz'] {
|
||||
if (!this.startDependencies?.fleetAuthzService) {
|
||||
throw new EndpointAppContentServicesNotStartedError();
|
||||
}
|
||||
|
||||
return this.startDependencies.fleetAuthzService;
|
||||
}
|
||||
|
||||
public async getEndpointAuthz(request: KibanaRequest): Promise<EndpointAuthz> {
|
||||
const fleetAuthz = await this.getFleetAuthzService().fromRequest(request);
|
||||
return calculateEndpointAuthz(this.getLicenseService(), fleetAuthz);
|
||||
}
|
||||
|
||||
public getEndpointMetadataService(): EndpointMetadataService {
|
||||
if (this.startDependencies == null) {
|
||||
throw new EndpointAppContentServicesNotStartedError();
|
||||
|
@ -198,4 +200,11 @@ export class EndpointAppContextService {
|
|||
}
|
||||
return this.startDependencies.cases.getCasesClientWithRequest(req);
|
||||
}
|
||||
|
||||
public getExceptionListsClient(): ExceptionListClient {
|
||||
if (!this.startDependencies?.exceptionListsClient) {
|
||||
throw new EndpointAppContentServicesNotStartedError();
|
||||
}
|
||||
return this.startDependencies.exceptionListsClient;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ import { ManifestManager } from './services/artifacts/manifest_manager/manifest_
|
|||
import { getManifestManagerMock } from './services/artifacts/manifest_manager/manifest_manager.mock';
|
||||
import { EndpointAppContext } from './types';
|
||||
import { MetadataRequestContext } from './routes/metadata/handlers';
|
||||
import { LicenseService } from '../../common/license';
|
||||
import { SecuritySolutionRequestHandlerContext } from '../types';
|
||||
import { parseExperimentalConfigValue } from '../../common/experimental_features';
|
||||
// A TS error (TS2403) is thrown when attempting to export the mock function below from Cases
|
||||
|
@ -45,6 +44,8 @@ import { createMockClients } from '../lib/detection_engine/routes/__mocks__/requ
|
|||
import { createEndpointMetadataServiceTestContextMock } from './services/metadata/mocks';
|
||||
|
||||
import type { EndpointAuthz } from '../../common/endpoint/types/authz';
|
||||
import { EndpointFleetServicesFactory } from './services/fleet';
|
||||
import { createLicenseServiceMock } from '../../common/license/mocks';
|
||||
|
||||
/**
|
||||
* Creates a mocked EndpointAppContext.
|
||||
|
@ -102,12 +103,22 @@ export const createMockEndpointAppContextServiceStartContract =
|
|||
const agentService = createMockAgentService();
|
||||
const agentPolicyService = createMockAgentPolicyService();
|
||||
const packagePolicyService = createPackagePolicyServiceMock();
|
||||
const packageService = createMockPackageService();
|
||||
const endpointMetadataService = new EndpointMetadataService(
|
||||
savedObjectsStart,
|
||||
agentPolicyService,
|
||||
packagePolicyService,
|
||||
logger
|
||||
);
|
||||
const endpointFleetServicesFactory = new EndpointFleetServicesFactory(
|
||||
{
|
||||
packageService,
|
||||
packagePolicyService,
|
||||
agentPolicyService,
|
||||
agentService,
|
||||
},
|
||||
savedObjectsStart
|
||||
);
|
||||
|
||||
packagePolicyService.list.mockImplementation(async (_, options) => {
|
||||
return {
|
||||
|
@ -122,14 +133,16 @@ export const createMockEndpointAppContextServiceStartContract =
|
|||
agentService,
|
||||
agentPolicyService,
|
||||
endpointMetadataService,
|
||||
endpointFleetServicesFactory,
|
||||
packagePolicyService,
|
||||
logger,
|
||||
packageService: createMockPackageService(),
|
||||
packageService,
|
||||
fleetAuthzService: createFleetAuthzServiceMock(),
|
||||
manifestManager: getManifestManagerMock(),
|
||||
security: securityMock.createStart(),
|
||||
alerting: alertsMock.createStart(),
|
||||
config,
|
||||
licenseService: new LicenseService(),
|
||||
licenseService: createLicenseServiceMock(),
|
||||
registerIngestCallback: jest.fn<
|
||||
ReturnType<FleetStartContract['registerExternalCallback']>,
|
||||
Parameters<FleetStartContract['registerExternalCallback']>
|
||||
|
@ -141,18 +154,21 @@ export const createMockEndpointAppContextServiceStartContract =
|
|||
};
|
||||
};
|
||||
|
||||
export const createFleetAuthzServiceMock = (): jest.Mocked<FleetStartContract['authz']> => {
|
||||
return {
|
||||
fromRequest: jest.fn(async (_) => createFleetAuthzMock()),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock IndexPatternService for use in tests that need to interact with the Fleet's
|
||||
* ESIndexPatternService.
|
||||
* Creates the Fleet Start contract mock return by the Fleet Plugin
|
||||
*
|
||||
* @param indexPattern a string index pattern to return when called by a test
|
||||
* @returns the same value as `indexPattern` parameter
|
||||
*/
|
||||
export const createMockFleetStartContract = (indexPattern: string): FleetStartContract => {
|
||||
return {
|
||||
authz: {
|
||||
fromRequest: jest.fn().mockResolvedValue(createFleetAuthzMock()),
|
||||
},
|
||||
authz: createFleetAuthzServiceMock(),
|
||||
fleetSetupCompleted: jest.fn().mockResolvedValue(undefined),
|
||||
esIndexPatternService: {
|
||||
getESIndexPattern: jest.fn().mockResolvedValue(indexPattern),
|
||||
|
|
|
@ -42,7 +42,7 @@ import {
|
|||
ENDPOINT_DEFAULT_PAGE_SIZE,
|
||||
METADATA_TRANSFORMS_PATTERN,
|
||||
} from '../../../../common/endpoint/constants';
|
||||
import { EndpointFleetServicesInterface } from '../../services/endpoint_fleet_services';
|
||||
import { EndpointFleetServicesInterface } from '../../services/fleet/endpoint_fleet_services_factory';
|
||||
|
||||
export interface MetadataRequestContext {
|
||||
esClient?: IScopedClusterClient;
|
||||
|
|
|
@ -11,11 +11,7 @@ import {
|
|||
createMockEndpointAppContextServiceStartContract,
|
||||
createRouteHandlerContext,
|
||||
} from '../../mocks';
|
||||
import {
|
||||
createMockAgentClient,
|
||||
createMockAgentService,
|
||||
createPackagePolicyServiceMock,
|
||||
} from '../../../../../fleet/server/mocks';
|
||||
import { createMockAgentClient, createMockAgentService } from '../../../../../fleet/server/mocks';
|
||||
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../fleet/common';
|
||||
import {
|
||||
getHostPolicyResponseHandler,
|
||||
|
@ -251,10 +247,20 @@ describe('test policy response handler', () => {
|
|||
let policyHandler: ReturnType<typeof getPolicyListHandler>;
|
||||
|
||||
beforeEach(() => {
|
||||
const endpointAppContextServiceStartContract =
|
||||
createMockEndpointAppContextServiceStartContract();
|
||||
|
||||
mockScopedClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockSavedObjectClient = savedObjectsClientMock.create();
|
||||
mockResponse = httpServerMock.createResponseFactory();
|
||||
mockPackagePolicyService = createPackagePolicyServiceMock();
|
||||
|
||||
if (endpointAppContextServiceStartContract.packagePolicyService) {
|
||||
mockPackagePolicyService =
|
||||
endpointAppContextServiceStartContract.packagePolicyService as jest.Mocked<PackagePolicyServiceInterface>;
|
||||
} else {
|
||||
expect(endpointAppContextServiceStartContract.packagePolicyService).toBeTruthy();
|
||||
}
|
||||
|
||||
mockPackagePolicyService.list.mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
items: [],
|
||||
|
@ -265,10 +271,7 @@ describe('test policy response handler', () => {
|
|||
});
|
||||
endpointAppContextService = new EndpointAppContextService();
|
||||
endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract());
|
||||
endpointAppContextService.start({
|
||||
...createMockEndpointAppContextServiceStartContract(),
|
||||
...{ packagePolicyService: mockPackagePolicyService },
|
||||
});
|
||||
endpointAppContextService.start(endpointAppContextServiceStartContract);
|
||||
policyHandler = getPolicyListHandler({
|
||||
logFactory: loggingSystemMock.create(),
|
||||
service: endpointAppContextService,
|
||||
|
@ -289,6 +292,7 @@ describe('test policy response handler', () => {
|
|||
mockRequest,
|
||||
mockResponse
|
||||
);
|
||||
expect(mockPackagePolicyService.list).toHaveBeenCalled();
|
||||
expect(mockPackagePolicyService.list.mock.calls[0][1]).toEqual({
|
||||
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint`,
|
||||
perPage: undefined,
|
||||
|
|
|
@ -5,14 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KibanaRequest } from 'kibana/server';
|
||||
import { KibanaRequest, SavedObjectsClientContract, SavedObjectsServiceStart } from 'kibana/server';
|
||||
import type {
|
||||
AgentClient,
|
||||
AgentPolicyServiceInterface,
|
||||
FleetStartContract,
|
||||
PackagePolicyServiceInterface,
|
||||
PackageClient,
|
||||
} from '../../../../fleet/server';
|
||||
} from '../../../../../fleet/server';
|
||||
import { createInternalReadonlySoClient } from '../../utils/create_internal_readonly_so_client';
|
||||
|
||||
export interface EndpointFleetServicesFactoryInterface {
|
||||
asScoped(req: KibanaRequest): EndpointScopedFleetServicesInterface;
|
||||
|
@ -25,7 +26,8 @@ export class EndpointFleetServicesFactory implements EndpointFleetServicesFactor
|
|||
private readonly fleetDependencies: Pick<
|
||||
FleetStartContract,
|
||||
'agentService' | 'packageService' | 'packagePolicyService' | 'agentPolicyService'
|
||||
>
|
||||
>,
|
||||
private savedObjectsStart: SavedObjectsServiceStart
|
||||
) {}
|
||||
|
||||
asScoped(req: KibanaRequest): EndpointScopedFleetServicesInterface {
|
||||
|
@ -61,6 +63,7 @@ export class EndpointFleetServicesFactory implements EndpointFleetServicesFactor
|
|||
packagePolicy,
|
||||
|
||||
asScoped: this.asScoped.bind(this),
|
||||
internalReadonlySoClient: createInternalReadonlySoClient(this.savedObjectsStart),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -87,4 +90,9 @@ export interface EndpointInternalFleetServicesInterface extends EndpointFleetSer
|
|||
* get scoped endpoint fleet services instance
|
||||
*/
|
||||
asScoped: EndpointFleetServicesFactoryInterface['asScoped'];
|
||||
|
||||
/**
|
||||
* An internal SO client (readonly) that can be used with the Fleet services that require it
|
||||
*/
|
||||
internalReadonlySoClient: SavedObjectsClientContract;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './endpoint_fleet_services_factory';
|
|
@ -56,7 +56,7 @@ import { getAllEndpointPackagePolicies } from '../../routes/metadata/support/end
|
|||
import { getAgentStatus } from '../../../../../fleet/common/services/agent_status';
|
||||
import { GetMetadataListRequestQuery } from '../../../../common/endpoint/schema/metadata';
|
||||
import { EndpointError } from '../../../../common/endpoint/errors';
|
||||
import { EndpointFleetServicesInterface } from '../endpoint_fleet_services';
|
||||
import { EndpointFleetServicesInterface } from '../fleet/endpoint_fleet_services_factory';
|
||||
|
||||
type AgentPolicyWithPackagePolicies = Omit<AgentPolicy, 'package_policies'> & {
|
||||
package_policies: PackagePolicy[];
|
||||
|
|
|
@ -21,7 +21,7 @@ import { AgentPolicyServiceInterface, AgentService } from '../../../../../fleet/
|
|||
import {
|
||||
EndpointFleetServicesFactory,
|
||||
EndpointInternalFleetServicesInterface,
|
||||
} from '../endpoint_fleet_services';
|
||||
} from '../fleet/endpoint_fleet_services_factory';
|
||||
|
||||
const createCustomizedPackagePolicyService = () => {
|
||||
const service = createPackagePolicyServiceMock();
|
||||
|
@ -62,12 +62,15 @@ export const createEndpointMetadataServiceTestContextMock = (
|
|||
.create()
|
||||
.get()
|
||||
): EndpointMetadataServiceTestContextMock => {
|
||||
const fleetServices = new EndpointFleetServicesFactory({
|
||||
agentService,
|
||||
packageService,
|
||||
packagePolicyService,
|
||||
agentPolicyService,
|
||||
}).asInternalUser();
|
||||
const fleetServices = new EndpointFleetServicesFactory(
|
||||
{
|
||||
agentService,
|
||||
packageService,
|
||||
packagePolicyService,
|
||||
agentPolicyService,
|
||||
},
|
||||
savedObjectsStart
|
||||
).asInternalUser();
|
||||
|
||||
const endpointMetadataService = new EndpointMetadataService(
|
||||
savedObjectsStart,
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
CreateExceptionListItemOptions,
|
||||
ExceptionsListPreCreateItemServerExtension,
|
||||
} from '../../../../../lists/server';
|
||||
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
|
||||
import { TrustedAppValidator } from '../validators';
|
||||
|
||||
export const getExceptionsPreCreateItemHandler = (
|
||||
endpointAppContext: EndpointAppContextService
|
||||
): ExceptionsListPreCreateItemServerExtension['callback'] => {
|
||||
return async function ({ data, context: { request } }): Promise<CreateExceptionListItemOptions> {
|
||||
// Validate trusted apps
|
||||
if (TrustedAppValidator.isTrustedApp(data)) {
|
||||
return new TrustedAppValidator(endpointAppContext, request).validatePreCreateItem(data);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
ExceptionsListPreUpdateItemServerExtension,
|
||||
UpdateExceptionListItemOptions,
|
||||
} from '../../../../../lists/server';
|
||||
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
|
||||
import { TrustedAppValidator } from '../validators';
|
||||
|
||||
export const getExceptionsPreUpdateItemHandler = (
|
||||
endpointAppContextService: EndpointAppContextService
|
||||
): ExceptionsListPreUpdateItemServerExtension['callback'] => {
|
||||
return async function ({ data, context: { request } }): Promise<UpdateExceptionListItemOptions> {
|
||||
const currentSavedItem = await endpointAppContextService
|
||||
.getExceptionListsClient()
|
||||
.getExceptionListItem({
|
||||
id: data.id,
|
||||
itemId: data.itemId,
|
||||
namespaceType: data.namespaceType,
|
||||
});
|
||||
|
||||
// We don't want to `throw` here becuase we don't know for sure that the item is one we care about.
|
||||
// So we just return the data and the Lists plugin will likely error out because it can't find the item
|
||||
if (!currentSavedItem) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Validate trusted apps
|
||||
if (TrustedAppValidator.isTrustedApp({ listId: currentSavedItem.list_id })) {
|
||||
return new TrustedAppValidator(endpointAppContextService, request).validatePreUpdateItem(
|
||||
data,
|
||||
currentSavedItem
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EndpointAppContextService } from '../../endpoint/endpoint_app_context_services';
|
||||
import { getExceptionsPreCreateItemHandler } from './handlers/exceptions_pre_create_handler';
|
||||
import { getExceptionsPreUpdateItemHandler } from './handlers/exceptions_pre_update_handler';
|
||||
import type { ListsServerExtensionRegistrar } from '../../../../lists/server';
|
||||
|
||||
export const registerListsPluginEndpointExtensionPoints = (
|
||||
registerListsExtensionPoint: ListsServerExtensionRegistrar,
|
||||
endpointAppContextService: EndpointAppContextService
|
||||
): void => {
|
||||
// PRE-CREATE handler
|
||||
registerListsExtensionPoint({
|
||||
type: 'exceptionsListPreCreateItem',
|
||||
callback: getExceptionsPreCreateItemHandler(endpointAppContextService),
|
||||
});
|
||||
|
||||
// PRE-UPDATE handler
|
||||
registerListsExtensionPoint({
|
||||
type: 'exceptionsListPreUpdateItem',
|
||||
callback: getExceptionsPreUpdateItemHandler(endpointAppContextService),
|
||||
});
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CreateExceptionListItemOptions } from '../../../../lists/server';
|
||||
|
||||
/**
|
||||
* An Exception Like item is a structure used internally by several of the Exceptions api/service in that
|
||||
* the keys are camelCased. Because different methods of the ExceptionListClient have slightly different
|
||||
* structures, this one attempt to normalize the properties we care about here that can be found across
|
||||
* those service methods.
|
||||
*/
|
||||
export type ExceptionItemLikeOptions = Pick<
|
||||
CreateExceptionListItemOptions,
|
||||
'osTypes' | 'tags' | 'description' | 'name' | 'entries' | 'namespaceType'
|
||||
> & { listId?: string };
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
|
||||
import {
|
||||
createMockEndpointAppContextServiceSetupContract,
|
||||
createMockEndpointAppContextServiceStartContract,
|
||||
} from '../../../endpoint/mocks';
|
||||
import { BaseValidatorMock, createExceptionItemLikeOptionsMock } from './mocks';
|
||||
import { EndpointArtifactExceptionValidationError } from './errors';
|
||||
import { httpServerMock } from '../../../../../../../src/core/server/mocks';
|
||||
import { createFleetAuthzMock, PackagePolicy } from '../../../../../fleet/common';
|
||||
import { PackagePolicyServiceInterface } from '../../../../../fleet/server';
|
||||
import { ExceptionItemLikeOptions } from '../types';
|
||||
import {
|
||||
BY_POLICY_ARTIFACT_TAG_PREFIX,
|
||||
GLOBAL_ARTIFACT_TAG,
|
||||
} from '../../../../common/endpoint/service/artifacts';
|
||||
|
||||
describe('When using Artifacts Exceptions BaseValidator', () => {
|
||||
let endpointAppContextServices: EndpointAppContextService;
|
||||
let kibanaRequest: ReturnType<typeof httpServerMock.createKibanaRequest>;
|
||||
let exceptionLikeItem: ExceptionItemLikeOptions;
|
||||
let validator: BaseValidatorMock;
|
||||
let packagePolicyService: jest.Mocked<PackagePolicyServiceInterface>;
|
||||
let initValidator: (withNoAuth?: boolean, withBasicLicense?: boolean) => BaseValidatorMock;
|
||||
|
||||
beforeEach(() => {
|
||||
kibanaRequest = httpServerMock.createKibanaRequest();
|
||||
exceptionLikeItem = createExceptionItemLikeOptionsMock();
|
||||
|
||||
const servicesStart = createMockEndpointAppContextServiceStartContract();
|
||||
|
||||
packagePolicyService =
|
||||
servicesStart.packagePolicyService as jest.Mocked<PackagePolicyServiceInterface>;
|
||||
|
||||
endpointAppContextServices = new EndpointAppContextService();
|
||||
endpointAppContextServices.setup(createMockEndpointAppContextServiceSetupContract());
|
||||
endpointAppContextServices.start(servicesStart);
|
||||
|
||||
initValidator = (withNoAuth: boolean = false, withBasicLicense = false) => {
|
||||
if (withNoAuth) {
|
||||
const fleetAuthz = createFleetAuthzMock();
|
||||
fleetAuthz.fleet.all = false;
|
||||
(servicesStart.fleetAuthzService?.fromRequest as jest.Mock).mockResolvedValue(fleetAuthz);
|
||||
}
|
||||
|
||||
if (withBasicLicense) {
|
||||
(servicesStart.licenseService.isPlatinumPlus as jest.Mock).mockResolvedValue(false);
|
||||
}
|
||||
|
||||
validator = new BaseValidatorMock(endpointAppContextServices, kibanaRequest);
|
||||
|
||||
return validator;
|
||||
};
|
||||
});
|
||||
|
||||
it('should use default endpoint authz (no access) when `request` is not provided', async () => {
|
||||
const baseValidator = new BaseValidatorMock(endpointAppContextServices);
|
||||
|
||||
await expect(baseValidator._isAllowedToCreateArtifactsByPolicy()).resolves.toBe(false);
|
||||
await expect(baseValidator._validateCanManageEndpointArtifacts()).rejects.toBeInstanceOf(
|
||||
EndpointArtifactExceptionValidationError
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate is allowed to manage endpoint artifacts', async () => {
|
||||
await expect(initValidator()._validateCanManageEndpointArtifacts()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw if not allowed to manage endpoint artifacts', async () => {
|
||||
await expect(initValidator(true)._validateCanManageEndpointArtifacts()).rejects.toBeInstanceOf(
|
||||
EndpointArtifactExceptionValidationError
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate basic artifact data', async () => {
|
||||
await expect(initValidator()._validateBasicData(exceptionLikeItem)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
'name is empty',
|
||||
() => {
|
||||
exceptionLikeItem.name = '';
|
||||
},
|
||||
],
|
||||
[
|
||||
'namespace is not agnostic',
|
||||
() => {
|
||||
exceptionLikeItem.namespaceType = 'single';
|
||||
},
|
||||
],
|
||||
[
|
||||
'osTypes has more than 1 value',
|
||||
() => {
|
||||
exceptionLikeItem.osTypes = ['macos', 'linux'];
|
||||
},
|
||||
],
|
||||
[
|
||||
'osType has invalid value',
|
||||
() => {
|
||||
exceptionLikeItem.osTypes = ['xunil' as 'linux'];
|
||||
},
|
||||
],
|
||||
])('should throw if %s', async (_, setupData) => {
|
||||
setupData();
|
||||
|
||||
await expect(initValidator()._validateBasicData(exceptionLikeItem)).rejects.toBeInstanceOf(
|
||||
EndpointArtifactExceptionValidationError
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate is allowed to create artifacts by policy', async () => {
|
||||
await expect(
|
||||
initValidator()._validateCanCreateByPolicyArtifacts(exceptionLikeItem)
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw if not allowed to create artifacts by policy', async () => {
|
||||
await expect(
|
||||
initValidator(false, true)._validateCanCreateByPolicyArtifacts(exceptionLikeItem)
|
||||
).rejects.toBeInstanceOf(EndpointArtifactExceptionValidationError);
|
||||
});
|
||||
|
||||
it('should validate policy ids for by policy artifacts', async () => {
|
||||
packagePolicyService.getByIDs.mockResolvedValue([
|
||||
{
|
||||
id: '123',
|
||||
version: '123',
|
||||
} as PackagePolicy,
|
||||
]);
|
||||
|
||||
await expect(initValidator()._validateByPolicyItem(exceptionLikeItem)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw if policy ids for by policy artifacts are not valid', async () => {
|
||||
packagePolicyService.getByIDs.mockResolvedValue([
|
||||
{
|
||||
id: '123',
|
||||
version: undefined,
|
||||
} as PackagePolicy,
|
||||
]);
|
||||
|
||||
await expect(initValidator()._validateByPolicyItem(exceptionLikeItem)).rejects.toBeInstanceOf(
|
||||
EndpointArtifactExceptionValidationError
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['no policies (unassigned)', () => createExceptionItemLikeOptionsMock({ tags: [] })],
|
||||
[
|
||||
'different policy',
|
||||
() => createExceptionItemLikeOptionsMock({ tags: [`${BY_POLICY_ARTIFACT_TAG_PREFIX}:456`] }),
|
||||
],
|
||||
[
|
||||
'additional policies',
|
||||
() =>
|
||||
createExceptionItemLikeOptionsMock({
|
||||
tags: [`${BY_POLICY_ARTIFACT_TAG_PREFIX}:123`, `${BY_POLICY_ARTIFACT_TAG_PREFIX}:456`],
|
||||
}),
|
||||
],
|
||||
])(
|
||||
'should return `true` when `wasByPolicyEffectScopeChanged()` is called with: %s',
|
||||
(_, getUpdated) => {
|
||||
expect(initValidator()._wasByPolicyEffectScopeChanged(getUpdated(), exceptionLikeItem)).toBe(
|
||||
true
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
['identical data', () => createExceptionItemLikeOptionsMock()],
|
||||
[
|
||||
'scope changed to all',
|
||||
() => createExceptionItemLikeOptionsMock({ tags: [GLOBAL_ARTIFACT_TAG] }),
|
||||
],
|
||||
])('should return `false` when `wasByPolicyEffectScopeChanged()` with: %s', (_, getUpdated) => {
|
||||
expect(initValidator()._wasByPolicyEffectScopeChanged(getUpdated(), exceptionLikeItem)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KibanaRequest } from 'kibana/server';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { isEqual } from 'lodash/fp';
|
||||
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
|
||||
import { ExceptionItemLikeOptions } from '../types';
|
||||
import { getEndpointAuthzInitialState } from '../../../../common/endpoint/service/authz';
|
||||
import {
|
||||
getPolicyIdsFromArtifact,
|
||||
isArtifactByPolicy,
|
||||
} from '../../../../common/endpoint/service/artifacts';
|
||||
import { OperatingSystem } from '../../../../common/endpoint/types';
|
||||
import { EndpointArtifactExceptionValidationError } from './errors';
|
||||
|
||||
const BasicEndpointExceptionDataSchema = schema.object(
|
||||
{
|
||||
// must have a name
|
||||
name: schema.string({ minLength: 1, maxLength: 256 }),
|
||||
|
||||
description: schema.maybe(schema.string({ minLength: 0, maxLength: 256, defaultValue: '' })),
|
||||
|
||||
// We only support agnostic entries
|
||||
namespaceType: schema.literal('agnostic'),
|
||||
|
||||
// only one OS per entry
|
||||
osTypes: schema.arrayOf(
|
||||
schema.oneOf([
|
||||
schema.literal(OperatingSystem.WINDOWS),
|
||||
schema.literal(OperatingSystem.LINUX),
|
||||
schema.literal(OperatingSystem.MAC),
|
||||
]),
|
||||
{ minSize: 1, maxSize: 1 }
|
||||
),
|
||||
},
|
||||
// Because we are only validating some fields from the Exception Item, we set `unknowns` to `ignore` here
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
|
||||
/**
|
||||
* Provides base methods for doing validation that apply across endpoint exception entries
|
||||
*/
|
||||
export class BaseValidator {
|
||||
private readonly endpointAuthzPromise: ReturnType<EndpointAppContextService['getEndpointAuthz']>;
|
||||
|
||||
constructor(
|
||||
protected readonly endpointAppContext: EndpointAppContextService,
|
||||
/**
|
||||
* Request is optional only because it needs to be optional in the Lists ExceptionListClient
|
||||
*/
|
||||
private readonly request?: KibanaRequest
|
||||
) {
|
||||
if (this.request) {
|
||||
this.endpointAuthzPromise = this.endpointAppContext.getEndpointAuthz(this.request);
|
||||
} else {
|
||||
this.endpointAuthzPromise = Promise.resolve(getEndpointAuthzInitialState());
|
||||
}
|
||||
}
|
||||
|
||||
protected isItemByPolicy(item: ExceptionItemLikeOptions): boolean {
|
||||
return isArtifactByPolicy(item);
|
||||
}
|
||||
|
||||
protected async isAllowedToCreateArtifactsByPolicy(): Promise<boolean> {
|
||||
return (await this.endpointAuthzPromise).canCreateArtifactsByPolicy;
|
||||
}
|
||||
|
||||
protected async validateCanManageEndpointArtifacts(): Promise<void> {
|
||||
if (!(await this.endpointAuthzPromise).canAccessEndpointManagement) {
|
||||
throw new EndpointArtifactExceptionValidationError('Endpoint authorization failure', 403);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* validates some basic common data that can be found across all endpoint exceptions
|
||||
* @param item
|
||||
* @protected
|
||||
*/
|
||||
protected async validateBasicData(item: ExceptionItemLikeOptions) {
|
||||
try {
|
||||
BasicEndpointExceptionDataSchema.validate(item);
|
||||
} catch (error) {
|
||||
throw new EndpointArtifactExceptionValidationError(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
protected async validateCanCreateByPolicyArtifacts(
|
||||
item: ExceptionItemLikeOptions
|
||||
): Promise<void> {
|
||||
if (this.isItemByPolicy(item) && !(await this.isAllowedToCreateArtifactsByPolicy())) {
|
||||
throw new EndpointArtifactExceptionValidationError(
|
||||
'Your license level does not allow create/update of by policy artifacts',
|
||||
403
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that by-policy artifacts is permitted and that each policy referenced in the item is valid
|
||||
* @protected
|
||||
*/
|
||||
protected async validateByPolicyItem(item: ExceptionItemLikeOptions): Promise<void> {
|
||||
if (this.isItemByPolicy(item)) {
|
||||
const { packagePolicy, internalReadonlySoClient } =
|
||||
this.endpointAppContext.getInternalFleetServices();
|
||||
const policyIds = getPolicyIdsFromArtifact(item);
|
||||
|
||||
if (policyIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const policiesFromFleet = await packagePolicy.getByIDs(internalReadonlySoClient, policyIds);
|
||||
|
||||
if (!policiesFromFleet) {
|
||||
throw new EndpointArtifactExceptionValidationError(
|
||||
`invalid policy ids: ${policyIds.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
const invalidPolicyIds = policiesFromFleet
|
||||
.filter((policy) => policy.version === undefined)
|
||||
.map((policy) => policy.id);
|
||||
|
||||
if (invalidPolicyIds.length) {
|
||||
throw new EndpointArtifactExceptionValidationError(
|
||||
`invalid policy ids: ${invalidPolicyIds.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the item being updated is `by policy`, method validates if anyting was changes in regard to
|
||||
* the effected scope of the by policy settings.
|
||||
*
|
||||
* @param updatedItem
|
||||
* @param currentItem
|
||||
* @protected
|
||||
*/
|
||||
protected wasByPolicyEffectScopeChanged(
|
||||
updatedItem: ExceptionItemLikeOptions,
|
||||
currentItem: Pick<ExceptionListItemSchema, 'tags'>
|
||||
): boolean {
|
||||
// if global, then return. Nothing to validate and setting the trusted app to global is allowed
|
||||
if (!this.isItemByPolicy(updatedItem)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (updatedItem.tags) {
|
||||
return !isEqual(
|
||||
getPolicyIdsFromArtifact({ tags: updatedItem.tags }),
|
||||
getPolicyIdsFromArtifact(currentItem)
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ListsErrorWithStatusCode } from '../../../../../lists/server';
|
||||
|
||||
export class EndpointArtifactExceptionValidationError extends ListsErrorWithStatusCode {
|
||||
constructor(message: string, statusCode: number = 400) {
|
||||
super(`EndpointArtifactError: ${message}`, statusCode);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { TrustedAppValidator } from './trusted_app_validator';
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { BaseValidator } from './base_validator';
|
||||
import { ExceptionItemLikeOptions } from '../types';
|
||||
import { listMock } from '../../../../../lists/server/mocks';
|
||||
import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../../common/endpoint/service/artifacts';
|
||||
|
||||
/**
|
||||
* Exposes all `protected` methods of `BaseValidator` by prefixing them with an underscore.
|
||||
*/
|
||||
export class BaseValidatorMock extends BaseValidator {
|
||||
_isItemByPolicy(item: ExceptionItemLikeOptions): boolean {
|
||||
return this.isItemByPolicy(item);
|
||||
}
|
||||
|
||||
async _isAllowedToCreateArtifactsByPolicy(): Promise<boolean> {
|
||||
return this.isAllowedToCreateArtifactsByPolicy();
|
||||
}
|
||||
|
||||
async _validateCanManageEndpointArtifacts(): Promise<void> {
|
||||
return this.validateCanManageEndpointArtifacts();
|
||||
}
|
||||
|
||||
async _validateBasicData(item: ExceptionItemLikeOptions) {
|
||||
return this.validateBasicData(item);
|
||||
}
|
||||
|
||||
async _validateCanCreateByPolicyArtifacts(item: ExceptionItemLikeOptions): Promise<void> {
|
||||
return this.validateCanCreateByPolicyArtifacts(item);
|
||||
}
|
||||
|
||||
async _validateByPolicyItem(item: ExceptionItemLikeOptions): Promise<void> {
|
||||
return this.validateByPolicyItem(item);
|
||||
}
|
||||
|
||||
_wasByPolicyEffectScopeChanged(
|
||||
updatedItem: ExceptionItemLikeOptions,
|
||||
currentItem: Pick<ExceptionListItemSchema, 'tags'>
|
||||
): boolean {
|
||||
return this.wasByPolicyEffectScopeChanged(updatedItem, currentItem);
|
||||
}
|
||||
}
|
||||
|
||||
export const createExceptionItemLikeOptionsMock = (
|
||||
overrides: Partial<ExceptionItemLikeOptions> = {}
|
||||
): ExceptionItemLikeOptions => {
|
||||
return {
|
||||
...listMock.getCreateExceptionListItemOptionsMock(),
|
||||
namespaceType: 'agnostic',
|
||||
osTypes: ['windows'],
|
||||
tags: [`${BY_POLICY_ARTIFACT_TAG_PREFIX}123`],
|
||||
...overrides,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { BaseValidator } from './base_validator';
|
||||
import { ExceptionItemLikeOptions } from '../types';
|
||||
import {
|
||||
CreateExceptionListItemOptions,
|
||||
UpdateExceptionListItemOptions,
|
||||
} from '../../../../../lists/server';
|
||||
import {
|
||||
ConditionEntry,
|
||||
OperatingSystem,
|
||||
TrustedAppEntryTypes,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import {
|
||||
getDuplicateFields,
|
||||
isValidHash,
|
||||
} from '../../../../common/endpoint/service/trusted_apps/validations';
|
||||
import { EndpointArtifactExceptionValidationError } from './errors';
|
||||
|
||||
const ProcessHashField = schema.oneOf([
|
||||
schema.literal('process.hash.md5'),
|
||||
schema.literal('process.hash.sha1'),
|
||||
schema.literal('process.hash.sha256'),
|
||||
]);
|
||||
const ProcessExecutablePath = schema.literal('process.executable.caseless');
|
||||
const ProcessCodeSigner = schema.literal('process.Ext.code_signature');
|
||||
|
||||
const ConditionEntryTypeSchema = schema.conditional(
|
||||
schema.siblingRef('field'),
|
||||
ProcessExecutablePath,
|
||||
schema.oneOf([schema.literal('match'), schema.literal('wildcard')]),
|
||||
schema.literal('match')
|
||||
);
|
||||
const ConditionEntryOperatorSchema = schema.literal('included');
|
||||
|
||||
type ConditionEntryFieldAllowedType =
|
||||
| TypeOf<typeof ProcessHashField>
|
||||
| TypeOf<typeof ProcessExecutablePath>
|
||||
| TypeOf<typeof ProcessCodeSigner>;
|
||||
|
||||
type TrustedAppConditionEntry<
|
||||
T extends ConditionEntryFieldAllowedType = ConditionEntryFieldAllowedType
|
||||
> =
|
||||
| {
|
||||
field: T;
|
||||
type: TrustedAppEntryTypes;
|
||||
operator: 'included';
|
||||
value: string;
|
||||
}
|
||||
| TypeOf<typeof WindowsSignerEntrySchema>;
|
||||
|
||||
/*
|
||||
* A generic Entry schema to be used for a specific entry schema depending on the OS
|
||||
*/
|
||||
const CommonEntrySchema = {
|
||||
field: schema.oneOf([ProcessHashField, ProcessExecutablePath]),
|
||||
type: ConditionEntryTypeSchema,
|
||||
operator: ConditionEntryOperatorSchema,
|
||||
// If field === HASH then validate hash with custom method, else validate string with minLength = 1
|
||||
value: schema.conditional(
|
||||
schema.siblingRef('field'),
|
||||
ProcessHashField,
|
||||
schema.string({
|
||||
validate: (hash: string) => (isValidHash(hash) ? undefined : `invalid hash value [${hash}]`),
|
||||
}),
|
||||
schema.conditional(
|
||||
schema.siblingRef('field'),
|
||||
ProcessExecutablePath,
|
||||
schema.string({
|
||||
validate: (pathValue: string) =>
|
||||
pathValue.length > 0 ? undefined : `invalid path value [${pathValue}]`,
|
||||
}),
|
||||
schema.string({
|
||||
validate: (signerValue: string) =>
|
||||
signerValue.length > 0 ? undefined : `invalid signer value [${signerValue}]`,
|
||||
})
|
||||
)
|
||||
),
|
||||
};
|
||||
|
||||
// Windows Signer entries use a Nested field that checks to ensure
|
||||
// that the certificate is trusted
|
||||
const WindowsSignerEntrySchema = schema.object({
|
||||
type: schema.literal('nested'),
|
||||
field: ProcessCodeSigner,
|
||||
entries: schema.arrayOf(
|
||||
schema.oneOf([
|
||||
schema.object({
|
||||
field: schema.literal('trusted'),
|
||||
value: schema.literal('true'),
|
||||
type: schema.literal('match'),
|
||||
operator: schema.literal('included'),
|
||||
}),
|
||||
schema.object({
|
||||
field: schema.literal('subject_name'),
|
||||
value: schema.string({ minLength: 1 }),
|
||||
type: schema.literal('match'),
|
||||
operator: schema.literal('included'),
|
||||
}),
|
||||
]),
|
||||
{ minSize: 2, maxSize: 2 }
|
||||
),
|
||||
});
|
||||
|
||||
const WindowsEntrySchema = schema.oneOf([
|
||||
WindowsSignerEntrySchema,
|
||||
schema.object({
|
||||
...CommonEntrySchema,
|
||||
field: schema.oneOf([ProcessHashField, ProcessExecutablePath]),
|
||||
}),
|
||||
]);
|
||||
|
||||
const LinuxEntrySchema = schema.object({
|
||||
...CommonEntrySchema,
|
||||
});
|
||||
|
||||
const MacEntrySchema = schema.object({
|
||||
...CommonEntrySchema,
|
||||
});
|
||||
|
||||
const entriesSchemaOptions = {
|
||||
minSize: 1,
|
||||
validate(entries: TrustedAppConditionEntry[]) {
|
||||
const dups = getDuplicateFields(entries as ConditionEntry[]);
|
||||
return dups.map((field) => `Duplicated entry: ${field}`).join(', ') || undefined;
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
* Entities array schema depending on Os type using schema.conditional.
|
||||
* If OS === WINDOWS then use Windows schema,
|
||||
* else if OS === LINUX then use Linux schema,
|
||||
* else use Mac schema
|
||||
*
|
||||
* The validate function checks there is no duplicated entry inside the array
|
||||
*/
|
||||
const EntriesSchema = schema.conditional(
|
||||
schema.contextRef('os'),
|
||||
OperatingSystem.WINDOWS,
|
||||
schema.arrayOf(WindowsEntrySchema, entriesSchemaOptions),
|
||||
schema.conditional(
|
||||
schema.contextRef('os'),
|
||||
OperatingSystem.LINUX,
|
||||
schema.arrayOf(LinuxEntrySchema, entriesSchemaOptions),
|
||||
schema.arrayOf(MacEntrySchema, entriesSchemaOptions)
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Schema to validate Trusted Apps data for create and update.
|
||||
* When called, it must be given an `context` with a `os` property set
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* TrustedAppDataSchema.validate(item, { os: 'windows' });
|
||||
*/
|
||||
const TrustedAppDataSchema = schema.object(
|
||||
{
|
||||
entries: EntriesSchema,
|
||||
},
|
||||
|
||||
// Because we are only validating some fields from the Exception Item, we set `unknowns` to `ignore` here
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
|
||||
export class TrustedAppValidator extends BaseValidator {
|
||||
static isTrustedApp(item: { listId: string }): boolean {
|
||||
return item.listId === ENDPOINT_TRUSTED_APPS_LIST_ID;
|
||||
}
|
||||
|
||||
async validatePreCreateItem(
|
||||
item: CreateExceptionListItemOptions
|
||||
): Promise<CreateExceptionListItemOptions> {
|
||||
await this.validateCanManageEndpointArtifacts();
|
||||
await this.validateTrustedAppData(item);
|
||||
await this.validateCanCreateByPolicyArtifacts(item);
|
||||
await this.validateByPolicyItem(item);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
async validatePreUpdateItem(
|
||||
_updatedItem: UpdateExceptionListItemOptions,
|
||||
currentItem: ExceptionListItemSchema
|
||||
): Promise<UpdateExceptionListItemOptions> {
|
||||
const updatedItem = _updatedItem as ExceptionItemLikeOptions;
|
||||
|
||||
await this.validateCanManageEndpointArtifacts();
|
||||
await this.validateTrustedAppData(updatedItem);
|
||||
|
||||
try {
|
||||
await this.validateCanCreateByPolicyArtifacts(updatedItem);
|
||||
} catch (noByPolicyAuthzError) {
|
||||
// Not allowed to create/update by policy data. Validate that the effective scope of the item
|
||||
// remained unchanged with this update or was set to `global` (only allowed update). If not,
|
||||
// then throw the validation error that was catch'ed
|
||||
if (this.wasByPolicyEffectScopeChanged(updatedItem, currentItem)) {
|
||||
throw noByPolicyAuthzError;
|
||||
}
|
||||
}
|
||||
|
||||
await this.validateByPolicyItem(updatedItem);
|
||||
|
||||
return updatedItem as UpdateExceptionListItemOptions;
|
||||
}
|
||||
|
||||
private async validateTrustedAppData(item: ExceptionItemLikeOptions): Promise<void> {
|
||||
await this.validateBasicData(item);
|
||||
|
||||
try {
|
||||
TrustedAppDataSchema.validate(item, { os: item.osTypes[0] });
|
||||
} catch (error) {
|
||||
throw new EndpointArtifactExceptionValidationError(error.message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { registerListsPluginEndpointExtensionPoints } from './endpoint/register_endpoint_extension_points';
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/x-pack/plugins/security_solution/server/lists_integration'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/security_solution/server/lists_integration',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/security_solution/server/lists_integration/**/*.{ts,tsx}',
|
||||
],
|
||||
// See: https://github.com/elastic/kibana/issues/117255, the moduleNameMapper creates mocks to avoid memory leaks from kibana core.
|
||||
moduleNameMapper: {
|
||||
'core/server$': '<rootDir>/x-pack/plugins/security_solution/server/__mocks__/core.mock.ts',
|
||||
'task_manager/server$':
|
||||
'<rootDir>/x-pack/plugins/security_solution/server/__mocks__/task_manager.mock.ts',
|
||||
'alerting/server$': '<rootDir>/x-pack/plugins/security_solution/server/__mocks__/alert.mock.ts',
|
||||
'actions/server$': '<rootDir>/x-pack/plugins/security_solution/server/__mocks__/action.mock.ts',
|
||||
},
|
||||
};
|
|
@ -90,6 +90,7 @@ import type {
|
|||
PluginInitializerContext,
|
||||
} from './plugin_contract';
|
||||
import { alertsFieldMap, rulesFieldMap } from '../common/field_maps';
|
||||
import { EndpointFleetServicesFactory } from './endpoint/services/fleet';
|
||||
|
||||
export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract';
|
||||
|
||||
|
@ -394,19 +395,31 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const exceptionListClient = this.lists!.getExceptionListClient(savedObjectsClient, 'kibana');
|
||||
|
||||
const { authz, agentService, packageService, packagePolicyService, agentPolicyService } =
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
plugins.fleet!;
|
||||
|
||||
this.endpointAppContextService.start({
|
||||
agentService: plugins.fleet?.agentService,
|
||||
packageService: plugins.fleet?.packageService,
|
||||
packagePolicyService: plugins.fleet?.packagePolicyService,
|
||||
agentPolicyService: plugins.fleet?.agentPolicyService,
|
||||
fleetAuthzService: authz,
|
||||
agentService,
|
||||
packageService,
|
||||
packagePolicyService,
|
||||
agentPolicyService,
|
||||
endpointMetadataService: new EndpointMetadataService(
|
||||
core.savedObjects,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
plugins.fleet?.agentPolicyService!,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
plugins.fleet?.packagePolicyService!,
|
||||
agentPolicyService,
|
||||
packagePolicyService,
|
||||
logger
|
||||
),
|
||||
endpointFleetServicesFactory: new EndpointFleetServicesFactory(
|
||||
{
|
||||
agentService,
|
||||
packageService,
|
||||
packagePolicyService,
|
||||
agentPolicyService,
|
||||
},
|
||||
core.savedObjects
|
||||
),
|
||||
security: plugins.security,
|
||||
alerting: plugins.alerting,
|
||||
config: this.config,
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ExceptionListItemSchema,
|
||||
CreateExceptionListSchema,
|
||||
CreateExceptionListItemSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
|
||||
import { Response } from 'superagent';
|
||||
import { FtrService } from '../../functional/ftr_provider_context';
|
||||
import { ExceptionsListItemGenerator } from '../../../plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator';
|
||||
import { TRUSTED_APPS_EXCEPTION_LIST_DEFINITION } from '../../../plugins/security_solution/public/management/pages/trusted_apps/constants';
|
||||
import { EndpointError } from '../../../plugins/security_solution/common/endpoint/errors';
|
||||
|
||||
export interface ArtifactTestData {
|
||||
artifact: ExceptionListItemSchema;
|
||||
cleanup: () => Promise<void>;
|
||||
}
|
||||
|
||||
export class EndpointArtifactsTestResources extends FtrService {
|
||||
private readonly exceptionsGenerator = new ExceptionsListItemGenerator();
|
||||
private readonly supertest = this.ctx.getService('supertest');
|
||||
private readonly log = this.ctx.getService('log');
|
||||
|
||||
private getHttpResponseFailureHandler(
|
||||
ignoredStatusCodes: number[] = []
|
||||
): (res: Response) => Promise<Response> {
|
||||
return async (res) => {
|
||||
if (!res.ok && !ignoredStatusCodes.includes(res.status)) {
|
||||
throw new EndpointError(JSON.stringify(res.error, null, 2));
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
private async ensureListExists(listDefinition: CreateExceptionListSchema): Promise<void> {
|
||||
// attempt to create it and ignore 409 (already exists) errors
|
||||
await this.supertest
|
||||
.post(EXCEPTION_LIST_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(listDefinition)
|
||||
.then(this.getHttpResponseFailureHandler([409]));
|
||||
}
|
||||
|
||||
private async createExceptionItem(
|
||||
createPayload: CreateExceptionListItemSchema
|
||||
): Promise<ArtifactTestData> {
|
||||
const artifact = await this.supertest
|
||||
.post(EXCEPTION_LIST_ITEM_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(createPayload)
|
||||
.then(this.getHttpResponseFailureHandler())
|
||||
.then((response) => response.body as ExceptionListItemSchema);
|
||||
|
||||
const { item_id: itemId, namespace_type: namespaceType } = artifact;
|
||||
|
||||
this.log.info(`Created exception list item: ${itemId}`);
|
||||
|
||||
const cleanup = async () => {
|
||||
const deleteResponse = await this.supertest
|
||||
.delete(`${EXCEPTION_LIST_ITEM_URL}?item_id=${itemId}&namespace_type=${namespaceType}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.then(this.getHttpResponseFailureHandler([404]));
|
||||
|
||||
this.log.info(`Deleted exception list item: ${itemId} (${deleteResponse.status})`);
|
||||
};
|
||||
|
||||
return {
|
||||
artifact,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
async createTrustedApp(
|
||||
overrides: Partial<CreateExceptionListItemSchema> = {}
|
||||
): Promise<ArtifactTestData> {
|
||||
await this.ensureListExists(TRUSTED_APPS_EXCEPTION_LIST_DEFINITION);
|
||||
const trustedApp = this.exceptionsGenerator.generateTrustedAppForCreate(overrides);
|
||||
|
||||
return this.createExceptionItem(trustedApp);
|
||||
}
|
||||
}
|
|
@ -10,10 +10,12 @@ import { EndpointPolicyTestResourcesProvider } from './endpoint_policy';
|
|||
import { IngestManagerProvider } from '../../common/services/ingest_manager';
|
||||
import { EndpointTelemetryTestResourcesProvider } from './endpoint_telemetry';
|
||||
import { EndpointTestResources } from './endpoint';
|
||||
import { EndpointArtifactsTestResources } from './endpoint_artifacts';
|
||||
|
||||
export const services = {
|
||||
...xPackFunctionalServices,
|
||||
endpointTestResources: EndpointTestResources,
|
||||
endpointArtifactTestResources: EndpointArtifactsTestResources,
|
||||
policyTestResources: EndpointPolicyTestResourcesProvider,
|
||||
telemetryTestResources: EndpointTelemetryTestResourcesProvider,
|
||||
ingestManager: IngestManagerProvider,
|
||||
|
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants';
|
||||
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
import { PolicyTestResourceInfo } from '../../security_solution_endpoint/services/endpoint_policy';
|
||||
import { ArtifactTestData } from '../../security_solution_endpoint/services/endpoint_artifacts';
|
||||
import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../plugins/security_solution/common/endpoint/service/artifacts';
|
||||
import { ExceptionsListItemGenerator } from '../../../plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator';
|
||||
import {
|
||||
createUserAndRole,
|
||||
deleteUserAndRole,
|
||||
ROLES,
|
||||
} from '../../common/services/security_solution';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const endpointPolicyTestResources = getService('endpointPolicyTestResources');
|
||||
const endpointArtifactTestResources = getService('endpointArtifactTestResources');
|
||||
|
||||
describe('Endpoint artifacts (via lists plugin)', () => {
|
||||
let fleetEndpointPolicy: PolicyTestResourceInfo;
|
||||
|
||||
before(async () => {
|
||||
// Create an endpoint policy in fleet we can work with
|
||||
fleetEndpointPolicy = await endpointPolicyTestResources.createPolicy();
|
||||
|
||||
// create role/user
|
||||
await createUserAndRole(getService, ROLES.detections_admin);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
if (fleetEndpointPolicy) {
|
||||
await fleetEndpointPolicy.cleanup();
|
||||
}
|
||||
|
||||
// delete role/user
|
||||
await deleteUserAndRole(getService, ROLES.detections_admin);
|
||||
});
|
||||
|
||||
const anEndpointArtifactError = (res: { body: { message: string } }) => {
|
||||
expect(res.body.message).to.match(/EndpointArtifactError/);
|
||||
};
|
||||
const anErrorMessageWith = (
|
||||
value: string | RegExp
|
||||
): ((res: { body: { message: string } }) => void) => {
|
||||
return (res) => {
|
||||
if (value instanceof RegExp) {
|
||||
expect(res.body.message).to.match(value);
|
||||
} else {
|
||||
expect(res.body.message).to.be(value);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
describe('and accessing trusted apps', () => {
|
||||
const exceptionsGenerator = new ExceptionsListItemGenerator();
|
||||
let trustedAppData: ArtifactTestData;
|
||||
|
||||
type TrustedAppApiCallsInterface = Array<{
|
||||
method: keyof Pick<typeof supertest, 'post' | 'put' | 'get' | 'delete' | 'patch'>;
|
||||
path: string;
|
||||
// The body just needs to have the properties we care about in the tests. This should cover most
|
||||
// mocks used for testing that support different interfaces
|
||||
getBody: () => Pick<ExceptionListItemSchema, 'os_types' | 'tags' | 'entries'>;
|
||||
}>;
|
||||
|
||||
beforeEach(async () => {
|
||||
trustedAppData = await endpointArtifactTestResources.createTrustedApp({
|
||||
tags: [`${BY_POLICY_ARTIFACT_TAG_PREFIX}${fleetEndpointPolicy.packagePolicy.id}`],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (trustedAppData) {
|
||||
await trustedAppData.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
const trustedAppApiCalls: TrustedAppApiCallsInterface = [
|
||||
{
|
||||
method: 'post',
|
||||
path: EXCEPTION_LIST_ITEM_URL,
|
||||
getBody: () => exceptionsGenerator.generateTrustedAppForCreate(),
|
||||
},
|
||||
{
|
||||
method: 'put',
|
||||
path: EXCEPTION_LIST_ITEM_URL,
|
||||
getBody: () =>
|
||||
exceptionsGenerator.generateTrustedAppForUpdate({
|
||||
id: trustedAppData.artifact.id,
|
||||
item_id: trustedAppData.artifact.item_id,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
describe('and has authorization to manage endpoint security', () => {
|
||||
for (const trustedAppApiCall of trustedAppApiCalls) {
|
||||
it(`should error on [${trustedAppApiCall.method}] if invalid condition entry fields are used`, async () => {
|
||||
const body = trustedAppApiCall.getBody();
|
||||
|
||||
body.entries[0].field = 'some.invalid.field';
|
||||
|
||||
await supertest[trustedAppApiCall.method](trustedAppApiCall.path)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(body)
|
||||
.expect(400)
|
||||
.expect(anEndpointArtifactError)
|
||||
.expect(anErrorMessageWith(/types that failed validation:/));
|
||||
});
|
||||
|
||||
it(`should error on [${trustedAppApiCall.method}] if a condition entry field is used more than once`, async () => {
|
||||
const body = trustedAppApiCall.getBody();
|
||||
|
||||
body.entries.push({ ...body.entries[0] });
|
||||
|
||||
await supertest[trustedAppApiCall.method](trustedAppApiCall.path)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(body)
|
||||
.expect(400)
|
||||
.expect(anEndpointArtifactError)
|
||||
.expect(anErrorMessageWith(/Duplicate/));
|
||||
});
|
||||
|
||||
it(`should error on [${trustedAppApiCall.method}] if an invalid hash is used`, async () => {
|
||||
const body = trustedAppApiCall.getBody();
|
||||
|
||||
body.entries = [
|
||||
{
|
||||
field: 'process.hash.md5',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: '1',
|
||||
},
|
||||
];
|
||||
|
||||
await supertest[trustedAppApiCall.method](trustedAppApiCall.path)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(body)
|
||||
.expect(400)
|
||||
.expect(anEndpointArtifactError)
|
||||
.expect(anErrorMessageWith(/invalid hash/));
|
||||
});
|
||||
|
||||
it(`should error on [${trustedAppApiCall.method}] if signer is set for a non windows os entry item`, async () => {
|
||||
const body = trustedAppApiCall.getBody();
|
||||
|
||||
body.os_types = ['linux'];
|
||||
body.entries = [
|
||||
{
|
||||
field: 'process.Ext.code_signature',
|
||||
entries: [
|
||||
{
|
||||
field: 'trusted',
|
||||
value: 'true',
|
||||
type: 'match',
|
||||
operator: 'included',
|
||||
},
|
||||
{
|
||||
field: 'subject_name',
|
||||
value: 'foo',
|
||||
type: 'match',
|
||||
operator: 'included',
|
||||
},
|
||||
],
|
||||
type: 'nested',
|
||||
},
|
||||
];
|
||||
|
||||
await supertest[trustedAppApiCall.method](trustedAppApiCall.path)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(body)
|
||||
.expect(400)
|
||||
.expect(anEndpointArtifactError)
|
||||
.expect(anErrorMessageWith(/^.*(?!process\.Ext\.code_signature)/));
|
||||
});
|
||||
|
||||
it(`should error on [${trustedAppApiCall.method}] if more than one OS is set`, async () => {
|
||||
const body = trustedAppApiCall.getBody();
|
||||
|
||||
body.os_types = ['linux', 'windows'];
|
||||
|
||||
await supertest[trustedAppApiCall.method](trustedAppApiCall.path)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(body)
|
||||
.expect(400)
|
||||
.expect(anEndpointArtifactError)
|
||||
.expect(anErrorMessageWith(/\[osTypes\]: array size is \[2\]/));
|
||||
});
|
||||
|
||||
it(`should error on [${trustedAppApiCall.method}] if policy id is invalid`, async () => {
|
||||
const body = trustedAppApiCall.getBody();
|
||||
|
||||
body.tags = [`${BY_POLICY_ARTIFACT_TAG_PREFIX}123`];
|
||||
|
||||
await supertest[trustedAppApiCall.method](trustedAppApiCall.path)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(body)
|
||||
.expect(400)
|
||||
.expect(anEndpointArtifactError)
|
||||
.expect(anErrorMessageWith(/invalid policy ids/));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('and user DOES NOT have authorization to manage endpoint security', () => {
|
||||
for (const trustedAppApiCall of trustedAppApiCalls) {
|
||||
it(`should error on [${trustedAppApiCall.method}]`, async () => {
|
||||
await supertestWithoutAuth[trustedAppApiCall.method](trustedAppApiCall.path)
|
||||
.auth(ROLES.detections_admin, 'changeme')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(trustedAppApiCall.getBody())
|
||||
.expect(403, {
|
||||
status_code: 403,
|
||||
message: 'EndpointArtifactError: Endpoint authorization failure',
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -32,5 +32,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider
|
|||
loadTestFile(require.resolve('./policy'));
|
||||
loadTestFile(require.resolve('./package'));
|
||||
loadTestFile(require.resolve('./endpoint_authz'));
|
||||
loadTestFile(require.resolve('./endpoint_artifacts'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,9 +8,13 @@
|
|||
import { services as xPackAPIServices } from '../../api_integration/services';
|
||||
import { ResolverGeneratorProvider } from './resolver';
|
||||
import { EndpointTestResources } from '../../security_solution_endpoint/services/endpoint';
|
||||
import { EndpointPolicyTestResourcesProvider } from '../../security_solution_endpoint/services/endpoint_policy';
|
||||
import { EndpointArtifactsTestResources } from '../../security_solution_endpoint/services/endpoint_artifacts';
|
||||
|
||||
export const services = {
|
||||
...xPackAPIServices,
|
||||
resolverGenerator: ResolverGeneratorProvider,
|
||||
endpointTestResources: EndpointTestResources,
|
||||
endpointPolicyTestResources: EndpointPolicyTestResourcesProvider,
|
||||
endpointArtifactTestResources: EndpointArtifactsTestResources,
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue