[8.0] [Security Solution][Endpoint] Move the UI Trusted Apps API calls to use the Exceptions List Items APIs (#118801) (#119632)

* [Security Solution][Endpoint] Move the UI Trusted Apps API calls to use the Exceptions List Items APIs (#118801)

* renamed TA service file to match name and added exports to index.ts
* Move `EndpointError` to top-level `common/endpoint/errors`
* add validation framework to TA service
* Mappers + Create service method changed to use Exceptions API
* Update Trusted app service method moved to use Exceptions API
* new generators

# Conflicts:
#	x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx
#	x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts
#	x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx

* Fix import issue due to PR that was not backported to 8.0
This commit is contained in:
Paul Tavares 2021-11-24 14:35:44 -05:00 committed by GitHub
parent 16f52941b8
commit 2d4cc5e18c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1335 additions and 586 deletions

View file

@ -11,6 +11,7 @@ import * as t from 'io-ts';
export const exceptionListType = t.keyof({
detection: null,
endpoint: null,
endpoint_trusted_apps: null,
endpoint_events: null,
endpoint_host_isolation_exceptions: null,
});
@ -20,6 +21,7 @@ export type ExceptionListTypeOrUndefined = t.TypeOf<typeof exceptionListTypeOrUn
export enum ExceptionListTypeEnum {
DETECTION = 'detection',
ENDPOINT = 'endpoint',
ENDPOINT_TRUSTED_APPS = 'endpoint',
ENDPOINT_EVENTS = 'endpoint_events',
ENDPOINT_HOST_ISOLATION_EXCEPTIONS = 'endpoint_host_isolation_exceptions',
}

View file

@ -86,7 +86,7 @@ describe('Lists', () => {
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events" | "endpoint_host_isolation_exceptions", namespace_type: "agnostic" | "single" |}>"',
'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_trusted_apps" | "endpoint_events" | "endpoint_host_isolation_exceptions", namespace_type: "agnostic" | "single" |}>"',
]);
expect(message.schema).toEqual({});
});
@ -117,8 +117,8 @@ describe('Lists', () => {
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events" | "endpoint_host_isolation_exceptions", namespace_type: "agnostic" | "single" |}> | undefined)"',
'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events" | "endpoint_host_isolation_exceptions", namespace_type: "agnostic" | "single" |}> | undefined)"',
'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_trusted_apps" | "endpoint_events" | "endpoint_host_isolation_exceptions", namespace_type: "agnostic" | "single" |}> | undefined)"',
'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_trusted_apps" | "endpoint_events" | "endpoint_host_isolation_exceptions", namespace_type: "agnostic" | "single" |}> | undefined)"',
]);
expect(message.schema).toEqual({});
});

View file

@ -0,0 +1,50 @@
/*
* 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 } from '@kbn/securitysolution-io-ts-list-types';
import { BaseDataGenerator } from './base_data_generator';
import { POLICY_REFERENCE_PREFIX } from '../service/trusted_apps/mapping';
import { ConditionEntryField } from '../types';
export class ExceptionsListItemGenerator extends BaseDataGenerator<ExceptionListItemSchema> {
generate(overrides: Partial<ExceptionListItemSchema> = {}): ExceptionListItemSchema {
return {
_version: this.randomString(5),
comments: [],
created_at: this.randomPastDate(),
created_by: this.randomUser(),
description: 'created by ExceptionListItemGenerator',
entries: [
{
field: ConditionEntryField.HASH,
operator: 'included',
type: 'match',
value: '1234234659af249ddf3e40864e9fb241',
},
{
field: ConditionEntryField.PATH,
operator: 'included',
type: 'match',
value: '/one/two/three',
},
],
id: this.seededUUIDv4(),
item_id: this.seededUUIDv4(),
list_id: 'endpoint_list_id',
meta: {},
name: `Generated Exception (${this.randomString(5)})`,
namespace_type: 'agnostic',
os_types: [this.randomOSFamily()] as ExceptionListItemSchema['os_types'],
tags: [`${POLICY_REFERENCE_PREFIX}all`],
tie_breaker_id: this.seededUUIDv4(),
type: 'simple',
updated_at: '2020-04-20T15:25:31.830Z',
updated_by: this.randomUser(),
...(overrides || {}),
};
}
}

View file

@ -0,0 +1,29 @@
/*
* 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 { BaseDataGenerator } from './base_data_generator';
import { agentPolicyStatuses, GetAgentPoliciesResponseItem } from '../../../../fleet/common';
export class FleetAgentPolicyGenerator extends BaseDataGenerator<GetAgentPoliciesResponseItem> {
generate(overrides: Partial<GetAgentPoliciesResponseItem> = {}): GetAgentPoliciesResponseItem {
return {
id: this.seededUUIDv4(),
name: `Agent Policy ${this.randomString(4)}`,
status: agentPolicyStatuses.Active,
description: 'Created by FleetAgentPolicyGenerator',
namespace: 'default',
is_managed: false,
monitoring_enabled: ['logs', 'metrics'],
revision: 2,
updated_at: '2020-07-22T16:36:49.196Z',
updated_by: this.randomUser(),
package_policies: ['852491f0-cc39-11ea-bac2-cdbf95b4b41a'],
agents: 0,
...overrides,
};
}
}

View file

@ -0,0 +1,73 @@
/*
* 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 { BaseDataGenerator } from './base_data_generator';
import { PackagePolicy } from '../../../../fleet/common';
import { policyFactory } from '../models/policy_config';
import { PolicyData } from '../types';
type PartialPackagePolicy = Partial<Omit<PackagePolicy, 'inputs'>> & {
inputs?: PackagePolicy['inputs'];
};
type PartialEndpointPolicyData = Partial<Omit<PolicyData, 'inputs'>> & {
inputs?: PolicyData['inputs'];
};
export class FleetPackagePolicyGenerator extends BaseDataGenerator<PackagePolicy> {
generate(overrides: PartialPackagePolicy = {}): PackagePolicy {
return {
id: this.seededUUIDv4(),
name: `Package Policy {${this.randomString(4)})`,
description: 'Policy to protect the worlds data',
created_at: this.randomPastDate(),
created_by: this.randomUser(),
updated_at: new Date().toISOString(),
updated_by: this.randomUser(),
policy_id: this.seededUUIDv4(), // agent policy id
enabled: true,
output_id: '',
inputs: [],
namespace: 'default',
package: {
name: 'endpoint',
title: 'Elastic Endpoint',
version: '1.0.0',
},
revision: 1,
...overrides,
};
}
generateEndpointPackagePolicy(overrides: PartialEndpointPolicyData = {}): PolicyData {
return {
...this.generate({
name: `Endpoint Policy {${this.randomString(4)})`,
}),
inputs: [
{
type: 'endpoint',
enabled: true,
streams: [],
config: {
artifact_manifest: {
value: {
manifest_version: '1.0.0',
schema_version: 'v1',
artifacts: {},
},
},
policy: {
value: policyFactory(),
},
},
},
],
...overrides,
};
}
}

View file

@ -0,0 +1,18 @@
/*
* 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.
*/
/**
* Endpoint base error class that supports an optional second argument for providing additional data
* for the error.
*/
export class EndpointError<MetaType = unknown> extends Error {
constructor(message: string, public readonly meta?: MetaType) {
super(message);
// For debugging - capture name of subclasses
this.name = this.constructor.name;
}
}

View file

@ -1535,6 +1535,7 @@ export class EndpointDocGenerator extends BaseDataGenerator {
*/
public generatePolicyPackagePolicy(): PolicyData {
const created = new Date(Date.now() - 8.64e7).toISOString(); // 24h ago
// FIXME: remove and use new FleetPackagePolicyGenerator (#2262)
return {
id: this.seededUUIDv4(),
name: 'Endpoint Policy',
@ -1579,6 +1580,7 @@ export class EndpointDocGenerator extends BaseDataGenerator {
* Generate an Agent Policy (ingest)
*/
public generateAgentPolicy(): GetAgentPoliciesResponseItem {
// FIXME: remove and use new FleetPackagePolicyGenerator (#2262)
return {
id: this.seededUUIDv4(),
name: 'Agent Policy',

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { fromKueryExpression } from '@kbn/es-query';
import {
httpHandlerMockFactory,
ResponseProvidersInterface,
@ -12,6 +13,7 @@ import {
import {
AGENT_API_ROUTES,
AGENT_POLICY_API_ROUTES,
AGENT_POLICY_SAVED_OBJECT_TYPE,
appRoutesService,
CheckPermissionsResponse,
EPM_API_ROUTES,
@ -22,6 +24,97 @@ import {
} from '../../../../../fleet/common';
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
import { GetPolicyListResponse, GetPolicyResponse } from '../policy/types';
import { FleetAgentPolicyGenerator } from '../../../../common/endpoint/data_generators/fleet_agent_policy_generator';
interface KqlArgumentType {
type: string;
value?: string | boolean;
function?: string;
arguments?: KqlArgumentType[];
}
const getPackagePoliciesFromKueryString = (kueryString: string): string[] => {
if (!kueryString) {
return [];
}
const kueryAst: ReturnType<typeof fromKueryExpression> & {
arguments?: KqlArgumentType[];
} = fromKueryExpression(kueryString);
/**
* # ABOUT THE STRUCTURE RETURNED BY THE KQL PARSER:
*
* The kuery AST has a structure similar to to this:
* given string:
*
* ingest-agent-policies.package_policies: (ddf6570b-9175-4a6d-b288-61a09771c647 or b8e616ae-44fc-4be7-846c-ce8fa5c082dd or 2d95bec3-b48f-4db7-9622-a2b061cc031d)
*
* output would be:
* {
* "type": "function",
* "function": "or", // this would not be here if no `OR` was found in the string
* "arguments": [
* {
* "type": "function",
* "function": "is",
* "arguments": [
* {
* "type": "literal",
* "value": "ingest-agent-policies.package_policies"
* },
* {
* "type": "literal",
* "value": "ddf6570b-9175-4a6d-b288-61a09771c647"
* },
* {
* "type": "literal",
* "value": false
* }
* ]
* },
* // .... other kquery arguments here
* ]
* }
*/
// Because there could be be many combinations of OR/AND, we just look for any defined literal that
// looks ot have a value for package_policies.
if (kueryAst.arguments) {
const packagePolicyIds: string[] = [];
const kqlArgumentQueue = [...kueryAst.arguments];
while (kqlArgumentQueue.length > 0) {
const kqlArgument = kqlArgumentQueue.shift();
if (kqlArgument) {
if (kqlArgument.arguments) {
kqlArgumentQueue.push(...kqlArgument.arguments);
}
if (
kqlArgument.type === 'literal' &&
kqlArgument.value === `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies`
) {
// If the next argument looks to be a value, then user it
const nextArgument = kqlArgumentQueue[0];
if (
nextArgument &&
nextArgument.type === 'literal' &&
'string' === typeof nextArgument.value
) {
packagePolicyIds.push(nextArgument.value);
kqlArgumentQueue.shift();
}
}
}
}
return packagePolicyIds;
}
return [];
};
export type FleetGetPackageListHttpMockInterface = ResponseProvidersInterface<{
packageList: () => GetPackagesResponse;
@ -70,6 +163,7 @@ export const fleetGetEndpointPackagePolicyListHttpMock =
path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN,
method: 'get',
handler: () => {
// FIXME: use new FleetPackagePolicyGenerator (#2262)
const generator = new EndpointDocGenerator('seed');
const items = Array.from({ length: 5 }, (_, index) => {
@ -97,23 +191,37 @@ export const fleetGetAgentPolicyListHttpMock =
id: 'agentPolicy',
path: AGENT_POLICY_API_ROUTES.LIST_PATTERN,
method: 'get',
handler: () => {
handler: ({ query }) => {
const generator = new EndpointDocGenerator('seed');
const agentPolicyGenerator = new FleetAgentPolicyGenerator('seed');
const endpointMetadata = generator.generateHostMetadata();
const agentPolicy = generator.generateAgentPolicy();
const requiredPolicyIds: string[] = [
// Make sure that the Agent policy returned from the API has the Integration Policy ID that
// the first endpoint metadata generated is using. This is needed especially when testing the
// Endpoint Details flyout where certain actions might be disabled if we know the endpoint integration policy no
// longer exists.
endpointMetadata.Endpoint.policy.applied.id,
// Make sure that the Agent policy returned from the API has the Integration Policy ID that
// the endpoint metadata is using. This is needed especially when testing the Endpoint Details
// flyout where certain actions might be disabled if we know the endpoint integration policy no
// longer exists.
(agentPolicy.package_policies as string[]).push(
endpointMetadata.Endpoint.policy.applied.id
);
// In addition, some of our UI logic looks for the existence of certain Endpoint Integration policies
// using the Agents Policy API (normally when checking IDs since query by ids is not supported via API)
// so also add the first two package policy IDs that the `fleetGetEndpointPackagePolicyListHttpMock()`
// method above creates (which Trusted Apps HTTP mocks also use)
// FIXME: remove hard-coded IDs below and get them from the new FleetPackagePolicyGenerator (#2262)
'ddf6570b-9175-4a6d-b288-61a09771c647',
'b8e616ae-44fc-4be7-846c-ce8fa5c082dd',
// And finally, include any kql filters for package policies ids
...getPackagePoliciesFromKueryString((query as { kuery?: string }).kuery ?? ''),
];
return {
items: [agentPolicy],
perPage: 10,
total: 1,
items: requiredPolicyIds.map((packagePolicyId) => {
return agentPolicyGenerator.generate({
package_policies: [packagePolicyId],
});
}),
perPage: Math.max(requiredPolicyIds.length, 10),
total: requiredPolicyIds.length,
page: 1,
};
},

View file

@ -7,48 +7,69 @@
import { HttpFetchOptionsWithPath } from 'kibana/public';
import {
ENDPOINT_TRUSTED_APPS_LIST_ID,
EXCEPTION_LIST_ITEM_URL,
EXCEPTION_LIST_URL,
} from '@kbn/securitysolution-list-constants';
import {
ExceptionListItemSchema,
FoundExceptionListItemSchema,
FindExceptionListItemSchema,
UpdateExceptionListItemSchema,
ReadExceptionListItemSchema,
CreateExceptionListItemSchema,
ExceptionListSchema,
} from '@kbn/securitysolution-io-ts-list-types';
import {
composeHttpHandlerMocks,
httpHandlerMockFactory,
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 {
GetTrustedAppsListRequest,
GetTrustedAppsListResponse,
PutTrustedAppUpdateRequest,
PutTrustedAppUpdateResponse,
} from '../../../../common/endpoint/types';
import {
TRUSTED_APPS_LIST_API,
TRUSTED_APPS_UPDATE_API,
} from '../../../../common/endpoint/constants';
import { TrustedAppGenerator } from '../../../../common/endpoint/data_generators/trusted_app_generator';
fleetGetAgentPolicyListHttpMock,
FleetGetAgentPolicyListHttpMockInterface,
fleetGetEndpointPackagePolicyListHttpMock,
FleetGetEndpointPackagePolicyListHttpMockInterface,
} from './fleet_mocks';
export type PolicyDetailsGetTrustedAppsListHttpMocksInterface = ResponseProvidersInterface<{
trustedAppsList: (options: HttpFetchOptionsWithPath) => GetTrustedAppsListResponse;
interface FindExceptionListItemSchemaQueryParams
extends Omit<FindExceptionListItemSchema, 'page' | 'per_page'> {
page?: number;
per_page?: number;
}
export type TrustedAppsGetListHttpMocksInterface = ResponseProvidersInterface<{
trustedAppsList: (options: HttpFetchOptionsWithPath) => FoundExceptionListItemSchema;
}>;
/**
* HTTP mock for retrieving list of Trusted Apps
*/
export const trustedAppsGetListHttpMocks =
httpHandlerMockFactory<PolicyDetailsGetTrustedAppsListHttpMocksInterface>([
httpHandlerMockFactory<TrustedAppsGetListHttpMocksInterface>([
{
id: 'trustedAppsList',
path: TRUSTED_APPS_LIST_API,
path: `${EXCEPTION_LIST_ITEM_URL}/_find`,
method: 'get',
handler: ({ query }): GetTrustedAppsListResponse => {
const apiQueryParams = query as GetTrustedAppsListRequest;
const generator = new TrustedAppGenerator('seed');
handler: ({ query }): FoundExceptionListItemSchema => {
const apiQueryParams = query as unknown as FindExceptionListItemSchemaQueryParams;
const generator = new ExceptionsListItemGenerator('seed');
const perPage = apiQueryParams.per_page ?? 10;
const data = Array.from({ length: Math.min(perPage, 50) }, () => generator.generate());
const data = Array.from({ length: Math.min(perPage, 50) }, () =>
generator.generate({ list_id: ENDPOINT_TRUSTED_APPS_LIST_ID })
);
// FIXME: remove hard-coded IDs below adn get them from the new FleetPackagePolicyGenerator (#2262)
// Change the 3rd entry (index 2) to be policy specific
data[2].effectScope = {
type: 'policy',
policies: [
// IDs below are those generated by the `fleetGetEndpointPackagePolicyListHttpMock()` mock
'ddf6570b-9175-4a6d-b288-61a09771c647',
'b8e616ae-44fc-4be7-846c-ce8fa5c082dd',
],
};
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`,
];
return {
page: apiQueryParams.page ?? 1,
@ -61,7 +82,7 @@ export const trustedAppsGetListHttpMocks =
]);
export type TrustedAppPutHttpMocksInterface = ResponseProvidersInterface<{
trustedAppUpdate: (options: HttpFetchOptionsWithPath) => PutTrustedAppUpdateResponse;
trustedAppUpdate: (options: HttpFetchOptionsWithPath) => ExceptionListItemSchema;
}>;
/**
* HTTP mocks that support updating a single Trusted Apps
@ -69,23 +90,113 @@ export type TrustedAppPutHttpMocksInterface = ResponseProvidersInterface<{
export const trustedAppPutHttpMocks = httpHandlerMockFactory<TrustedAppPutHttpMocksInterface>([
{
id: 'trustedAppUpdate',
path: TRUSTED_APPS_UPDATE_API,
path: EXCEPTION_LIST_ITEM_URL,
method: 'put',
handler: ({ body, path }): PutTrustedAppUpdateResponse => {
const response: PutTrustedAppUpdateResponse = {
data: {
...(body as unknown as PutTrustedAppUpdateRequest),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
id: path.split('/').pop()!,
created_at: '2021-10-12T16:02:55.856Z',
created_by: 'elastic',
updated_at: '2021-10-13T16:02:55.856Z',
updated_by: 'elastic',
version: 'abc',
},
handler: ({ body, path }): ExceptionListItemSchema => {
const updatedExceptionItem = JSON.parse(
body as string
) as Required<UpdateExceptionListItemSchema>;
const response: ExceptionListItemSchema = {
...updatedExceptionItem,
id: path.split('/').pop() ?? 'unknown-id',
comments: [],
created_at: '2021-10-12T16:02:55.856Z',
created_by: 'elastic',
updated_at: '2021-10-13T16:02:55.856Z',
updated_by: 'elastic',
list_id: ENDPOINT_TRUSTED_APPS_LIST_ID,
_version: 'abc',
tie_breaker_id: '1111',
};
return response;
},
},
]);
export type TrustedAppsGetOneHttpMocksInterface = ResponseProvidersInterface<{
trustedApp: (options: HttpFetchOptionsWithPath) => ExceptionListItemSchema;
}>;
/**
* HTTP mock for retrieving list of Trusted Apps
*/
export const trustedAppsGetOneHttpMocks =
httpHandlerMockFactory<TrustedAppsGetOneHttpMocksInterface>([
{
id: 'trustedApp',
path: EXCEPTION_LIST_ITEM_URL,
method: 'get',
handler: ({ query }): ExceptionListItemSchema => {
const apiQueryParams = query as ReadExceptionListItemSchema;
const exceptionItem = new ExceptionsListItemGenerator('seed').generate();
exceptionItem.item_id = apiQueryParams.item_id ?? exceptionItem.item_id;
exceptionItem.namespace_type =
apiQueryParams.namespace_type ?? exceptionItem.namespace_type;
return exceptionItem;
},
},
]);
export type TrustedAppPostHttpMocksInterface = ResponseProvidersInterface<{
trustedAppCreate: (options: HttpFetchOptionsWithPath) => ExceptionListItemSchema;
}>;
/**
* HTTP mocks that support updating a single Trusted Apps
*/
export const trustedAppPostHttpMocks = httpHandlerMockFactory<TrustedAppPostHttpMocksInterface>([
{
id: 'trustedAppCreate',
path: EXCEPTION_LIST_ITEM_URL,
method: 'post',
handler: ({ body, path }): ExceptionListItemSchema => {
const { comments, ...updatedExceptionItem } = JSON.parse(
body as string
) as CreateExceptionListItemSchema;
const response: ExceptionListItemSchema = {
...new ExceptionsListItemGenerator('seed').generate(),
...updatedExceptionItem,
};
response.id = path.split('/').pop() ?? response.id;
return response;
},
},
]);
export type TrustedAppsPostCreateListHttpMockInterface = ResponseProvidersInterface<{
trustedAppCreateList: (options: HttpFetchOptionsWithPath) => ExceptionListSchema;
}>;
/**
* HTTP mocks that support updating a single Trusted Apps
*/
export const trustedAppsPostCreateListHttpMock =
httpHandlerMockFactory<TrustedAppsPostCreateListHttpMockInterface>([
{
id: 'trustedAppCreateList',
path: EXCEPTION_LIST_URL,
method: 'post',
handler: (): ExceptionListSchema => {
return getTrustedAppsListSchemaMock();
},
},
]);
export type TrustedAppsAllHttpMocksInterface = FleetGetEndpointPackagePolicyListHttpMockInterface &
FleetGetAgentPolicyListHttpMockInterface &
TrustedAppsGetListHttpMocksInterface &
TrustedAppsGetOneHttpMocksInterface &
TrustedAppPutHttpMocksInterface &
TrustedAppPostHttpMocksInterface &
TrustedAppsPostCreateListHttpMockInterface;
/** Use this HTTP mock when wanting to mock the API calls done by the Trusted Apps Http service */
export const trustedAppsAllHttpMocks = composeHttpHandlerMocks<TrustedAppsAllHttpMocksInterface>([
trustedAppsGetListHttpMocks,
trustedAppsGetOneHttpMocks,
trustedAppPutHttpMocks,
trustedAppPostHttpMocks,
trustedAppsPostCreateListHttpMock,
fleetGetEndpointPackagePolicyListHttpMock,
fleetGetAgentPolicyListHttpMock,
]);

View file

@ -14,15 +14,16 @@ import { TrustedAppsHttpService } from '../../../../trusted_apps/service';
export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory<PolicyDetailsState> = (
coreStart
) => {
// Initialize services needed by Policy middleware
const trustedAppsService = new TrustedAppsHttpService(coreStart.http);
const middlewareContext: MiddlewareRunnerContext = {
coreStart,
trustedAppsService,
};
return (store) => (next) => async (action) => {
next(action);
const trustedAppsService = new TrustedAppsHttpService(coreStart.http);
const middlewareContext: MiddlewareRunnerContext = {
coreStart,
trustedAppsService,
};
policySettingsMiddlewareRunner(middlewareContext, store, action);
policyTrustedAppsMiddlewareRunner(middlewareContext, store, action);
};

View file

@ -23,9 +23,15 @@ import {
fleetGetEndpointPackagePolicyListHttpMock,
FleetGetEndpointPackagePolicyListHttpMockInterface,
trustedAppsGetListHttpMocks,
PolicyDetailsGetTrustedAppsListHttpMocksInterface,
TrustedAppsGetListHttpMocksInterface,
trustedAppPutHttpMocks,
TrustedAppPutHttpMocksInterface,
trustedAppsGetOneHttpMocks,
TrustedAppsGetOneHttpMocksInterface,
fleetGetAgentPolicyListHttpMock,
FleetGetAgentPolicyListHttpMockInterface,
trustedAppsPostCreateListHttpMock,
TrustedAppsPostCreateListHttpMockInterface,
} from '../../mocks';
export const getMockListResponse: () => GetTrustedAppsListResponse = () => ({
@ -71,13 +77,19 @@ export type PolicyDetailsPageAllApiHttpMocksInterface =
FleetGetEndpointPackagePolicyHttpMockInterface &
FleetGetAgentStatusHttpMockInterface &
FleetGetEndpointPackagePolicyListHttpMockInterface &
PolicyDetailsGetTrustedAppsListHttpMocksInterface &
TrustedAppPutHttpMocksInterface;
FleetGetAgentPolicyListHttpMockInterface &
TrustedAppsGetListHttpMocksInterface &
TrustedAppPutHttpMocksInterface &
TrustedAppsGetOneHttpMocksInterface &
TrustedAppsPostCreateListHttpMockInterface;
export const policyDetailsPageAllApiHttpMocks =
composeHttpHandlerMocks<PolicyDetailsPageAllApiHttpMocksInterface>([
fleetGetEndpointPackagePolicyHttpMock,
fleetGetAgentStatusHttpMock,
fleetGetEndpointPackagePolicyListHttpMock,
fleetGetAgentPolicyListHttpMock,
trustedAppsGetListHttpMocks,
trustedAppPutHttpMocks,
trustedAppsGetOneHttpMocks,
trustedAppsPostCreateListHttpMock,
]);

View file

@ -14,39 +14,26 @@ import {
} from '../../../../../../common/mock/endpoint';
import { MiddlewareActionSpyHelper } from '../../../../../../common/store/test_utils';
import { TrustedAppsHttpService } from '../../../../trusted_apps/service';
import { PolicyDetailsState } from '../../../types';
import { getMockCreateResponse, getMockListResponse } from '../../../test_utils';
import { createLoadedResourceState, isLoadedResourceState } from '../../../../../state';
import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing';
import { trustedAppsAllHttpMocks } from '../../../../mocks';
import { HttpFetchOptionsWithPath } from 'kibana/public';
jest.mock('../../../../trusted_apps/service');
jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges');
let mockedContext: AppContextTestRender;
let waitForAction: MiddlewareActionSpyHelper['waitForAction'];
let render: () => ReturnType<AppContextTestRender['render']>;
let mockedApis: ReturnType<typeof trustedAppsAllHttpMocks>;
const act = reactTestingLibrary.act;
const TrustedAppsHttpServiceMock = TrustedAppsHttpService as jest.Mock;
let getState: () => PolicyDetailsState;
describe('Policy trusted apps flyout', () => {
beforeEach(() => {
TrustedAppsHttpServiceMock.mockImplementation(() => {
return {
getTrustedAppsList: () => getMockListResponse(),
updateTrustedApp: () => ({
data: getMockCreateResponse(),
}),
assignPolicyToTrustedApps: () => [
{
data: getMockCreateResponse(),
},
],
};
});
mockedContext = createAppRootMockRenderer();
waitForAction = mockedContext.middlewareSpy.waitForAction;
mockedApis = trustedAppsAllHttpMocks(mockedContext.coreStart.http);
getState = () => mockedContext.store.getState().management.policyDetails;
render = () => mockedContext.render(<PolicyTrustedAppsFlyout />);
});
@ -58,11 +45,13 @@ describe('Policy trusted apps flyout', () => {
validate: (action) => isLoadedResourceState(action.payload),
});
TrustedAppsHttpServiceMock.mockImplementation(() => {
return {
getTrustedAppsList: () => ({ data: [] }),
};
mockedApis.responseProvider.trustedAppsList.mockReturnValue({
data: [],
total: 0,
per_page: 10,
page: 1,
});
const component = render();
mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234', { show: 'list' }));
@ -77,10 +66,11 @@ describe('Policy trusted apps flyout', () => {
});
it('should renders flyout open correctly without data', async () => {
TrustedAppsHttpServiceMock.mockImplementation(() => {
return {
getTrustedAppsList: () => ({ data: [] }),
};
mockedApis.responseProvider.trustedAppsList.mockReturnValue({
data: [],
total: 0,
per_page: 10,
page: 1,
});
const component = render();
@ -107,36 +97,37 @@ describe('Policy trusted apps flyout', () => {
});
expect(component.getByTestId('confirmPolicyTrustedAppsFlyout')).not.toBeNull();
expect(component.getByTestId(`${getMockListResponse().data[0].name}_checkbox`)).not.toBeNull();
expect(component.getByTestId('Generated Exception (u6kh2)_checkbox')).not.toBeNull();
});
it('should confirm flyout action', async () => {
const waitForUpdate = waitForAction('policyArtifactsUpdateTrustedAppsChanged', {
validate: (action) => isLoadedResourceState(action.payload),
});
const waitChangeUrl = waitForAction('userChangedUrl');
const component = render();
mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234', { show: 'list' }));
mockedContext.history.push(
getPolicyDetailsArtifactsListPath('2d95bec3-b48f-4db7-9622-a2b061cc031d', { show: 'list' })
);
await waitForAction('policyArtifactsAssignableListPageDataChanged', {
validate: (action) => isLoadedResourceState(action.payload),
});
const tACardCheckbox = component.getByTestId(`${getMockListResponse().data[0].name}_checkbox`);
// TA name below in the selector matches the 3rd generated trusted app which is policy specific
const tACardCheckbox = component.getByTestId('Generated Exception (3xnng)_checkbox');
await act(async () => {
act(() => {
fireEvent.click(tACardCheckbox);
});
const waitChangeUrl = waitForAction('userChangedUrl');
const confirmButton = component.getByTestId('confirmPolicyTrustedAppsFlyout');
await act(async () => {
act(() => {
fireEvent.click(confirmButton);
});
await waitForUpdate;
await waitChangeUrl;
const currentLocation = getState().artifacts.location;
expect(currentLocation.show).toBeUndefined();
});
@ -161,11 +152,13 @@ describe('Policy trusted apps flyout', () => {
});
it('should display warning message when too much results', async () => {
TrustedAppsHttpServiceMock.mockImplementation(() => {
return {
getTrustedAppsList: () => ({ ...getMockListResponse(), total: 101 }),
};
});
const listResponse = {
...mockedApis.responseProvider.trustedAppsList.getMockImplementation()!({
query: {},
} as HttpFetchOptionsWithPath),
total: 101,
};
mockedApis.responseProvider.trustedAppsList.mockReturnValue(listResponse);
const component = render();

View file

@ -13,32 +13,32 @@ import {
} from '../../../../../../common/mock/endpoint';
import { MiddlewareActionSpyHelper } from '../../../../../../common/store/test_utils';
import { TrustedAppsHttpService } from '../../../../trusted_apps/service';
import { getMockListResponse } from '../../../test_utils';
import { createLoadedResourceState, isLoadedResourceState } from '../../../../../state';
import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing';
import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data';
import { policyListApiPathHandlers } from '../../../store/test_mock_utils';
import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges';
import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks';
import { trustedAppsAllHttpMocks } from '../../../../mocks';
jest.mock('../../../../trusted_apps/service');
jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges');
const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock;
let mockedContext: AppContextTestRender;
let waitForAction: MiddlewareActionSpyHelper['waitForAction'];
let render: () => ReturnType<AppContextTestRender['render']>;
const TrustedAppsHttpServiceMock = TrustedAppsHttpService as jest.Mock;
let coreStart: AppContextTestRender['coreStart'];
let http: typeof coreStart.http;
let mockedApis: ReturnType<typeof trustedAppsAllHttpMocks>;
const generator = new EndpointDocGenerator();
describe('Policy trusted apps layout', () => {
beforeEach(() => {
mockedContext = createAppRootMockRenderer();
http = mockedContext.coreStart.http;
const policyListApiHandlers = policyListApiPathHandlers();
http.get.mockImplementation((...args) => {
const [path] = args;
if (typeof path === 'string') {
@ -67,12 +67,8 @@ describe('Policy trusted apps layout', () => {
return Promise.reject(new Error(`unknown API call (not MOCKED): ${path}`));
});
TrustedAppsHttpServiceMock.mockImplementation(() => {
return {
getTrustedAppsList: () => ({ data: [] }),
};
});
mockedApis = trustedAppsAllHttpMocks(http);
waitForAction = mockedContext.middlewareSpy.waitForAction;
render = () => mockedContext.render(<PolicyTrustedAppsLayout />);
});
@ -84,9 +80,14 @@ describe('Policy trusted apps layout', () => {
afterEach(() => reactTestingLibrary.cleanup());
it('should renders layout with no existing TA data', async () => {
const component = render();
mockedApis.responseProvider.trustedAppsList.mockImplementation(() => ({
data: [],
page: 1,
per_page: 10,
total: 0,
}));
mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234'));
const component = render();
await waitForAction('policyArtifactsDeosAnyTrustedAppExists', {
validate: (action) => isLoadedResourceState(action.payload),
@ -96,8 +97,14 @@ describe('Policy trusted apps layout', () => {
});
it('should renders layout with no assigned TA data', async () => {
const component = render();
mockedApis.responseProvider.trustedAppsList.mockImplementation(() => ({
data: [],
page: 1,
per_page: 10,
total: 0,
}));
mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234'));
const component = render();
await waitForAction('assignedTrustedAppsListStateChanged');
@ -110,13 +117,8 @@ describe('Policy trusted apps layout', () => {
});
it('should renders layout with data', async () => {
TrustedAppsHttpServiceMock.mockImplementation(() => {
return {
getTrustedAppsList: () => getMockListResponse(),
};
});
const component = render();
mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234'));
const component = render();
await waitForAction('assignedTrustedAppsListStateChanged');
@ -147,11 +149,6 @@ describe('Policy trusted apps layout', () => {
isPlatinumPlus: false,
})
);
TrustedAppsHttpServiceMock.mockImplementation(() => {
return {
getTrustedAppsList: () => getMockListResponse(),
};
});
const component = render();
mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234'));

View file

@ -202,7 +202,7 @@ describe('when rendering the PolicyTrustedAppsList', () => {
expect(appTestContext.coreStart.application.navigateToApp).toHaveBeenCalledWith(
APP_UI_ID,
expect.objectContaining({
path: '/administration/trusted_apps?filter=89f72d8a-05b5-4350-8cad-0dc3661d6e67',
path: '/administration/trusted_apps?filter=6f12b025-fcb0-4db4-99e5-4927e3502bb8',
})
);
});
@ -324,12 +324,12 @@ describe('when rendering the PolicyTrustedAppsList', () => {
});
it('does not show remove option in actions menu if license is downgraded to gold or below', async () => {
await render();
mockUseEndpointPrivileges.mockReturnValue(
loadedUserEndpointPrivilegesState({
isPlatinumPlus: false,
})
);
await render();
await toggleCardActionMenu(POLICY_SPECIFIC_CARD_INDEX);
expect(renderResult.queryByTestId('policyTrustedAppsGrid-removeAction')).toBeNull();

View file

@ -24,6 +24,7 @@ import {
} from '../../../store/policy_details/action/policy_trusted_apps_action';
import { Immutable } from '../../../../../../../common/endpoint/types';
import { HttpFetchOptionsWithPath } from 'kibana/public';
import { exceptionListItemToTrustedApp } from '../../../../trusted_apps/service/mappers';
describe('When using the RemoveTrustedAppFromPolicyModal component', () => {
let appTestContext: AppContextTestRender;
@ -41,8 +42,10 @@ describe('When using the RemoveTrustedAppFromPolicyModal component', () => {
mockedApis = policyDetailsPageAllApiHttpMocks(appTestContext.coreStart.http);
trustedApps = [
// The 3rd trusted app generated by the HTTP mock is a Policy Specific one
mockedApis.responseProvider.trustedAppsList({ query: {} } as HttpFetchOptionsWithPath)
.data[2],
exceptionListItemToTrustedApp(
mockedApis.responseProvider.trustedAppsList({ query: {} } as HttpFetchOptionsWithPath)
.data[2]
),
];
// Delay the Update Trusted App API response so that we can test UI states while the update is underway.
@ -211,7 +214,7 @@ describe('When using the RemoveTrustedAppFromPolicyModal component', () => {
await clickConfirmButton(true, true);
expect(appTestContext.coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({
text: '"Avast Business Antivirus" has been removed from Endpoint Policy policy',
text: '"Generated Exception (3xnng)" has been removed from Endpoint Policy policy',
title: 'Successfully removed',
});
});

View file

@ -5,6 +5,16 @@
* 2.0.
*/
import {
CreateExceptionListSchema,
ExceptionListTypeEnum,
} from '@kbn/securitysolution-io-ts-list-types';
import {
ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION,
ENDPOINT_TRUSTED_APPS_LIST_ID,
ENDPOINT_TRUSTED_APPS_LIST_NAME,
} from '@kbn/securitysolution-list-constants';
export const SEARCHABLE_FIELDS: Readonly<string[]> = [
`name`,
`description`,
@ -12,3 +22,11 @@ export const SEARCHABLE_FIELDS: Readonly<string[]> = [
`entries.value`,
`entries.entries.value`,
];
export const TRUSTED_APPS_EXCEPTION_LIST_DEFINITION: CreateExceptionListSchema = {
name: ENDPOINT_TRUSTED_APPS_LIST_NAME,
namespace_type: 'agnostic',
description: ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION,
list_id: ENDPOINT_TRUSTED_APPS_LIST_ID,
type: ExceptionListTypeEnum.ENDPOINT_TRUSTED_APPS,
};

View file

@ -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 { EndpointError } from '../../../../../common/endpoint/errors';
export class HttpRequestValidationError extends EndpointError<string[]> {
public readonly body: { message: string };
constructor(validationFailures: string[]) {
super('Invalid trusted application', validationFailures);
// Attempts to mirror an HTTP API error body
this.body = {
message: validationFailures.join(', ') ?? 'unknown',
};
}
}

View file

@ -5,186 +5,4 @@
* 2.0.
*/
import { HttpStart } from 'kibana/public';
import pMap from 'p-map';
import {
TRUSTED_APPS_CREATE_API,
TRUSTED_APPS_DELETE_API,
TRUSTED_APPS_GET_API,
TRUSTED_APPS_LIST_API,
TRUSTED_APPS_UPDATE_API,
TRUSTED_APPS_SUMMARY_API,
} from '../../../../../common/endpoint/constants';
import {
DeleteTrustedAppsRequestParams,
GetTrustedAppsListResponse,
GetTrustedAppsListRequest,
PostTrustedAppCreateRequest,
PostTrustedAppCreateResponse,
GetTrustedAppsSummaryResponse,
PutTrustedAppUpdateRequest,
PutTrustedAppUpdateResponse,
PutTrustedAppsRequestParams,
GetOneTrustedAppRequestParams,
GetOneTrustedAppResponse,
GetTrustedAppsSummaryRequest,
TrustedApp,
MaybeImmutable,
} from '../../../../../common/endpoint/types';
import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables';
import { sendGetEndpointSpecificPackagePolicies } from '../../policy/store/services/ingest';
import { toUpdateTrustedApp } from '../../../../../common/endpoint/service/trusted_apps/to_update_trusted_app';
import { isGlobalEffectScope } from '../state/type_guards';
export interface TrustedAppsService {
getTrustedApp(params: GetOneTrustedAppRequestParams): Promise<GetOneTrustedAppResponse>;
getTrustedAppsList(request: GetTrustedAppsListRequest): Promise<GetTrustedAppsListResponse>;
deleteTrustedApp(request: DeleteTrustedAppsRequestParams): Promise<void>;
createTrustedApp(request: PostTrustedAppCreateRequest): Promise<PostTrustedAppCreateResponse>;
updateTrustedApp(
params: PutTrustedAppsRequestParams,
request: PutTrustedAppUpdateRequest
): Promise<PutTrustedAppUpdateResponse>;
getPolicyList(
options?: Parameters<typeof sendGetEndpointSpecificPackagePolicies>[1]
): ReturnType<typeof sendGetEndpointSpecificPackagePolicies>;
assignPolicyToTrustedApps(
policyId: string,
trustedApps: MaybeImmutable<TrustedApp[]>
): Promise<PutTrustedAppUpdateResponse[]>;
removePolicyFromTrustedApps(
policyId: string,
trustedApps: MaybeImmutable<TrustedApp[]>
): Promise<PutTrustedAppUpdateResponse[]>;
}
const P_MAP_OPTIONS = Object.freeze<pMap.Options>({
concurrency: 5,
/** When set to false, instead of stopping when a promise rejects, it will wait for all the promises to settle
* and then reject with an aggregated error containing all the errors from the rejected promises. */
stopOnError: false,
});
export class TrustedAppsHttpService implements TrustedAppsService {
constructor(private http: HttpStart) {}
async getTrustedApp(params: GetOneTrustedAppRequestParams) {
return this.http.get<GetOneTrustedAppResponse>(
resolvePathVariables(TRUSTED_APPS_GET_API, params)
);
}
async getTrustedAppsList(request: GetTrustedAppsListRequest) {
return this.http.get<GetTrustedAppsListResponse>(TRUSTED_APPS_LIST_API, {
query: request,
});
}
async deleteTrustedApp(request: DeleteTrustedAppsRequestParams): Promise<void> {
return this.http.delete<void>(resolvePathVariables(TRUSTED_APPS_DELETE_API, request));
}
async createTrustedApp(request: PostTrustedAppCreateRequest) {
return this.http.post<PostTrustedAppCreateResponse>(TRUSTED_APPS_CREATE_API, {
body: JSON.stringify(request),
});
}
async updateTrustedApp(
params: PutTrustedAppsRequestParams,
updatedTrustedApp: PutTrustedAppUpdateRequest
) {
return this.http.put<PutTrustedAppUpdateResponse>(
resolvePathVariables(TRUSTED_APPS_UPDATE_API, params),
{ body: JSON.stringify(updatedTrustedApp) }
);
}
async getTrustedAppsSummary(request: GetTrustedAppsSummaryRequest) {
return this.http.get<GetTrustedAppsSummaryResponse>(TRUSTED_APPS_SUMMARY_API, {
query: request,
});
}
getPolicyList(options?: Parameters<typeof sendGetEndpointSpecificPackagePolicies>[1]) {
return sendGetEndpointSpecificPackagePolicies(this.http, options);
}
/**
* Assign a policy to trusted apps. Note that Trusted Apps MUST NOT be global
*
* @param policyId
* @param trustedApps[]
*/
assignPolicyToTrustedApps(
policyId: string,
trustedApps: MaybeImmutable<TrustedApp[]>
): Promise<PutTrustedAppUpdateResponse[]> {
return this._handleAssignOrRemovePolicyId('assign', policyId, trustedApps);
}
/**
* Remove a policy from trusted apps. Note that Trusted Apps MUST NOT be global
*
* @param policyId
* @param trustedApps[]
*/
removePolicyFromTrustedApps(
policyId: string,
trustedApps: MaybeImmutable<TrustedApp[]>
): Promise<PutTrustedAppUpdateResponse[]> {
return this._handleAssignOrRemovePolicyId('remove', policyId, trustedApps);
}
private _handleAssignOrRemovePolicyId(
action: 'assign' | 'remove',
policyId: string,
trustedApps: MaybeImmutable<TrustedApp[]>
): Promise<PutTrustedAppUpdateResponse[]> {
if (policyId.trim() === '') {
throw new Error('policy ID is required');
}
if (trustedApps.length === 0) {
throw new Error('at least one trusted app is required');
}
return pMap(
trustedApps,
async (trustedApp) => {
if (isGlobalEffectScope(trustedApp.effectScope)) {
throw new Error(
`Unable to update trusted app [${trustedApp.id}] policy assignment. It's effectScope is 'global'`
);
}
const policies: string[] = !isGlobalEffectScope(trustedApp.effectScope)
? [...trustedApp.effectScope.policies]
: [];
const indexOfPolicyId = policies.indexOf(policyId);
if (action === 'assign' && indexOfPolicyId === -1) {
policies.push(policyId);
} else if (action === 'remove' && indexOfPolicyId !== -1) {
policies.splice(indexOfPolicyId, 1);
}
return this.updateTrustedApp(
{ id: trustedApp.id },
{
...toUpdateTrustedApp(trustedApp),
effectScope: {
type: 'policy',
policies,
},
}
);
},
P_MAP_OPTIONS
);
}
}
export * from './trusted_apps_http_service';

View file

@ -0,0 +1,266 @@
/*
* 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 {
CreateExceptionListItemSchema,
EntriesArray,
EntryMatch,
EntryMatchWildcard,
EntryNested,
ExceptionListItemSchema,
NestedEntriesArray,
OsType,
UpdateExceptionListItemSchema,
} from '@kbn/securitysolution-io-ts-list-types';
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants';
import {
ConditionEntry,
ConditionEntryField,
EffectScope,
NewTrustedApp,
OperatingSystem,
TrustedApp,
TrustedAppEntryTypes,
UpdateTrustedApp,
} from '../../../../../common/endpoint/types';
import {
POLICY_REFERENCE_PREFIX,
tagsToEffectScope,
} from '../../../../../common/endpoint/service/trusted_apps/mapping';
type ConditionEntriesMap = { [K in ConditionEntryField]?: ConditionEntry<K> };
type Mapping<T extends string, U> = { [K in T]: U };
const OS_TYPE_TO_OPERATING_SYSTEM: Mapping<OsType, OperatingSystem> = {
linux: OperatingSystem.LINUX,
macos: OperatingSystem.MAC,
windows: OperatingSystem.WINDOWS,
};
const OPERATING_SYSTEM_TO_OS_TYPE: Mapping<OperatingSystem, OsType> = {
[OperatingSystem.LINUX]: 'linux',
[OperatingSystem.MAC]: 'macos',
[OperatingSystem.WINDOWS]: 'windows',
};
const OPERATOR_VALUE = 'included';
const filterUndefined = <T>(list: Array<T | undefined>): T[] => {
return list.filter((item: T | undefined): item is T => item !== undefined);
};
const createConditionEntry = <T extends ConditionEntryField>(
field: T,
type: TrustedAppEntryTypes,
value: string
): ConditionEntry<T> => {
return { field, value, type, operator: OPERATOR_VALUE };
};
const entriesToConditionEntriesMap = (entries: EntriesArray): ConditionEntriesMap => {
return entries.reduce((result, entry) => {
if (entry.field.startsWith('process.hash') && entry.type === 'match') {
return {
...result,
[ConditionEntryField.HASH]: createConditionEntry(
ConditionEntryField.HASH,
entry.type,
entry.value
),
};
} else if (
entry.field === 'process.executable.caseless' &&
(entry.type === 'match' || entry.type === 'wildcard')
) {
return {
...result,
[ConditionEntryField.PATH]: createConditionEntry(
ConditionEntryField.PATH,
entry.type,
entry.value
),
};
} else if (entry.field === 'process.Ext.code_signature' && entry.type === 'nested') {
const subjectNameCondition = entry.entries.find((subEntry): subEntry is EntryMatch => {
return subEntry.field === 'subject_name' && subEntry.type === 'match';
});
if (subjectNameCondition) {
return {
...result,
[ConditionEntryField.SIGNER]: createConditionEntry(
ConditionEntryField.SIGNER,
subjectNameCondition.type,
subjectNameCondition.value
),
};
}
}
return result;
}, {} as ConditionEntriesMap);
};
/**
* Map an ExceptionListItem to a TrustedApp item
* @param exceptionListItem
*/
export const exceptionListItemToTrustedApp = (
exceptionListItem: ExceptionListItemSchema
): TrustedApp => {
if (exceptionListItem.os_types[0]) {
const os = osFromExceptionItem(exceptionListItem);
const grouped = entriesToConditionEntriesMap(exceptionListItem.entries);
return {
id: exceptionListItem.item_id,
version: exceptionListItem._version || '',
name: exceptionListItem.name,
description: exceptionListItem.description,
effectScope: tagsToEffectScope(exceptionListItem.tags),
created_at: exceptionListItem.created_at,
created_by: exceptionListItem.created_by,
updated_at: exceptionListItem.updated_at,
updated_by: exceptionListItem.updated_by,
...(os === OperatingSystem.LINUX || os === OperatingSystem.MAC
? {
os,
entries: filterUndefined([
grouped[ConditionEntryField.HASH],
grouped[ConditionEntryField.PATH],
]),
}
: {
os,
entries: filterUndefined([
grouped[ConditionEntryField.HASH],
grouped[ConditionEntryField.PATH],
grouped[ConditionEntryField.SIGNER],
]),
}),
};
} else {
throw new Error('Unknown Operating System assigned to trusted application.');
}
};
const osFromExceptionItem = (exceptionListItem: ExceptionListItemSchema): TrustedApp['os'] => {
return OS_TYPE_TO_OPERATING_SYSTEM[exceptionListItem.os_types[0]];
};
const hashType = (hash: string): 'md5' | 'sha256' | 'sha1' | undefined => {
switch (hash.length) {
case 32:
return 'md5';
case 40:
return 'sha1';
case 64:
return 'sha256';
}
};
const createEntryMatch = (field: string, value: string): EntryMatch => {
return { field, value, type: 'match', operator: OPERATOR_VALUE };
};
const createEntryMatchWildcard = (field: string, value: string): EntryMatchWildcard => {
return { field, value, type: 'wildcard', operator: OPERATOR_VALUE };
};
const createEntryNested = (field: string, entries: NestedEntriesArray): EntryNested => {
return { field, entries, type: 'nested' };
};
const effectScopeToTags = (effectScope: EffectScope) => {
if (effectScope.type === 'policy') {
return effectScope.policies.map((policy) => `${POLICY_REFERENCE_PREFIX}${policy}`);
} else {
return [`${POLICY_REFERENCE_PREFIX}all`];
}
};
const conditionEntriesToEntries = (conditionEntries: ConditionEntry[]): EntriesArray => {
return conditionEntries.map((conditionEntry) => {
if (conditionEntry.field === ConditionEntryField.HASH) {
return createEntryMatch(
`process.hash.${hashType(conditionEntry.value)}`,
conditionEntry.value.toLowerCase()
);
} else if (conditionEntry.field === ConditionEntryField.SIGNER) {
return createEntryNested(`process.Ext.code_signature`, [
createEntryMatch('trusted', 'true'),
createEntryMatch('subject_name', conditionEntry.value),
]);
} else if (
conditionEntry.field === ConditionEntryField.PATH &&
conditionEntry.type === 'wildcard'
) {
return createEntryMatchWildcard(`process.executable.caseless`, conditionEntry.value);
} else {
return createEntryMatch(`process.executable.caseless`, conditionEntry.value);
}
});
};
/**
* Map NewTrustedApp to CreateExceptionListItemOptions.
*/
export const newTrustedAppToCreateExceptionListItem = ({
os,
entries,
name,
description = '',
effectScope,
}: NewTrustedApp): CreateExceptionListItemSchema => {
return {
comments: [],
description,
entries: conditionEntriesToEntries(entries),
list_id: ENDPOINT_TRUSTED_APPS_LIST_ID,
meta: undefined,
name,
namespace_type: 'agnostic',
os_types: [OPERATING_SYSTEM_TO_OS_TYPE[os]],
tags: effectScopeToTags(effectScope),
type: 'simple',
};
};
/**
* Map UpdateTrustedApp to UpdateExceptionListItemOptions
*
* @param {ExceptionListItemSchema} currentTrustedAppExceptionItem
* @param {UpdateTrustedApp} updatedTrustedApp
*/
export const updatedTrustedAppToUpdateExceptionListItem = (
{
id,
item_id: itemId,
namespace_type: namespaceType,
type,
comments,
meta,
}: ExceptionListItemSchema,
{ os, entries, name, description = '', effectScope, version }: UpdateTrustedApp
): UpdateExceptionListItemSchema => {
return {
_version: version,
name,
description,
entries: conditionEntriesToEntries(entries),
os_types: [OPERATING_SYSTEM_TO_OS_TYPE[os]],
tags: effectScopeToTags(effectScope),
// Copied from current trusted app exception item
id,
comments,
item_id: itemId,
meta,
namespace_type: namespaceType,
type,
};
};

View file

@ -0,0 +1,288 @@
/*
* 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 pMap from 'p-map';
import { HttpStart } from 'kibana/public';
import {
ENDPOINT_TRUSTED_APPS_LIST_ID,
EXCEPTION_LIST_ITEM_URL,
EXCEPTION_LIST_URL,
} from '@kbn/securitysolution-list-constants';
import {
ExceptionListItemSchema,
ExceptionListSchema,
ExceptionListSummarySchema,
FoundExceptionListItemSchema,
} from '@kbn/securitysolution-io-ts-list-types';
import {
DeleteTrustedAppsRequestParams,
GetOneTrustedAppRequestParams,
GetOneTrustedAppResponse,
GetTrustedAppsListRequest,
GetTrustedAppsListResponse,
GetTrustedAppsSummaryRequest,
MaybeImmutable,
PostTrustedAppCreateRequest,
PostTrustedAppCreateResponse,
PutTrustedAppsRequestParams,
PutTrustedAppUpdateRequest,
PutTrustedAppUpdateResponse,
TrustedApp,
} from '../../../../../common/endpoint/types';
import { sendGetEndpointSpecificPackagePolicies } from '../../policy/store/services/ingest';
import { isGlobalEffectScope } from '../state/type_guards';
import { toUpdateTrustedApp } from '../../../../../common/endpoint/service/trusted_apps/to_update_trusted_app';
import { validateTrustedAppHttpRequestBody } from './validate_trusted_app_http_request_body';
import {
exceptionListItemToTrustedApp,
newTrustedAppToCreateExceptionListItem,
updatedTrustedAppToUpdateExceptionListItem,
} from './mappers';
import { TRUSTED_APPS_EXCEPTION_LIST_DEFINITION } from '../constants';
export interface TrustedAppsService {
getTrustedApp(params: GetOneTrustedAppRequestParams): Promise<GetOneTrustedAppResponse>;
getTrustedAppsList(request: GetTrustedAppsListRequest): Promise<GetTrustedAppsListResponse>;
deleteTrustedApp(request: DeleteTrustedAppsRequestParams): Promise<void>;
createTrustedApp(request: PostTrustedAppCreateRequest): Promise<PostTrustedAppCreateResponse>;
updateTrustedApp(
params: PutTrustedAppsRequestParams,
request: PutTrustedAppUpdateRequest
): Promise<PutTrustedAppUpdateResponse>;
getPolicyList(
options?: Parameters<typeof sendGetEndpointSpecificPackagePolicies>[1]
): ReturnType<typeof sendGetEndpointSpecificPackagePolicies>;
assignPolicyToTrustedApps(
policyId: string,
trustedApps: MaybeImmutable<TrustedApp[]>
): Promise<PutTrustedAppUpdateResponse[]>;
removePolicyFromTrustedApps(
policyId: string,
trustedApps: MaybeImmutable<TrustedApp[]>
): Promise<PutTrustedAppUpdateResponse[]>;
}
const P_MAP_OPTIONS = Object.freeze<pMap.Options>({
concurrency: 5,
/** When set to false, instead of stopping when a promise rejects, it will wait for all the promises to settle
* and then reject with an aggregated error containing all the errors from the rejected promises. */
stopOnError: false,
});
export class TrustedAppsHttpService implements TrustedAppsService {
private readonly getHttpService: () => Promise<HttpStart>;
constructor(http: HttpStart) {
let ensureListExists: Promise<void>;
this.getHttpService = async () => {
if (!ensureListExists) {
ensureListExists = http
.post<ExceptionListSchema>(EXCEPTION_LIST_URL, {
body: JSON.stringify(TRUSTED_APPS_EXCEPTION_LIST_DEFINITION),
})
.then(() => {})
.catch((err) => {
if (err.response.status !== 409) {
return Promise.reject(err);
}
});
}
await ensureListExists;
return http;
};
}
private async getExceptionListItem(itemId: string): Promise<ExceptionListItemSchema> {
return (await this.getHttpService()).get<ExceptionListItemSchema>(EXCEPTION_LIST_ITEM_URL, {
query: {
item_id: itemId,
namespace_type: 'agnostic',
},
});
}
async getTrustedApp(params: GetOneTrustedAppRequestParams) {
const exceptionItem = await this.getExceptionListItem(params.id);
return {
data: exceptionListItemToTrustedApp(exceptionItem),
};
}
async getTrustedAppsList({
page = 1,
per_page: perPage = 10,
kuery,
}: GetTrustedAppsListRequest): Promise<GetTrustedAppsListResponse> {
const itemListResults = await (
await this.getHttpService()
).get<FoundExceptionListItemSchema>(`${EXCEPTION_LIST_ITEM_URL}/_find`, {
query: {
page,
per_page: perPage,
filter: kuery,
sort_field: 'name',
sort_order: 'asc',
list_id: [ENDPOINT_TRUSTED_APPS_LIST_ID],
namespace_type: ['agnostic'],
},
});
return {
...itemListResults,
data: itemListResults.data.map(exceptionListItemToTrustedApp),
};
}
async deleteTrustedApp(request: DeleteTrustedAppsRequestParams): Promise<void> {
await (
await this.getHttpService()
).delete<ExceptionListItemSchema>(EXCEPTION_LIST_ITEM_URL, {
query: {
item_id: request.id,
namespace_type: 'agnostic',
},
});
}
async createTrustedApp(request: PostTrustedAppCreateRequest) {
await validateTrustedAppHttpRequestBody(await this.getHttpService(), request);
const newTrustedAppException = newTrustedAppToCreateExceptionListItem(request);
const createdExceptionItem = await (
await this.getHttpService()
).post<ExceptionListItemSchema>(EXCEPTION_LIST_ITEM_URL, {
body: JSON.stringify(newTrustedAppException),
});
return {
data: exceptionListItemToTrustedApp(createdExceptionItem),
};
}
async updateTrustedApp(
params: PutTrustedAppsRequestParams,
updatedTrustedApp: PutTrustedAppUpdateRequest
) {
const [currentExceptionListItem] = await Promise.all([
await this.getExceptionListItem(params.id),
await validateTrustedAppHttpRequestBody(await this.getHttpService(), updatedTrustedApp),
]);
const updatedExceptionListItem = await (
await this.getHttpService()
).put<ExceptionListItemSchema>(EXCEPTION_LIST_ITEM_URL, {
body: JSON.stringify(
updatedTrustedAppToUpdateExceptionListItem(currentExceptionListItem, updatedTrustedApp)
),
});
return {
data: exceptionListItemToTrustedApp(updatedExceptionListItem),
};
}
async getTrustedAppsSummary(_: GetTrustedAppsSummaryRequest) {
return (await this.getHttpService()).get<ExceptionListSummarySchema>(
`${EXCEPTION_LIST_URL}/summary`,
{
query: {
list_id: ENDPOINT_TRUSTED_APPS_LIST_ID,
namespace_type: 'agnostic',
},
}
);
}
async getPolicyList(options?: Parameters<typeof sendGetEndpointSpecificPackagePolicies>[1]) {
return sendGetEndpointSpecificPackagePolicies(await this.getHttpService(), options);
}
/**
* Assign a policy to trusted apps. Note that Trusted Apps MUST NOT be global
*
* @param policyId
* @param trustedApps[]
*/
assignPolicyToTrustedApps(
policyId: string,
trustedApps: MaybeImmutable<TrustedApp[]>
): Promise<PutTrustedAppUpdateResponse[]> {
return this._handleAssignOrRemovePolicyId('assign', policyId, trustedApps);
}
/**
* Remove a policy from trusted apps. Note that Trusted Apps MUST NOT be global
*
* @param policyId
* @param trustedApps[]
*/
removePolicyFromTrustedApps(
policyId: string,
trustedApps: MaybeImmutable<TrustedApp[]>
): Promise<PutTrustedAppUpdateResponse[]> {
return this._handleAssignOrRemovePolicyId('remove', policyId, trustedApps);
}
private _handleAssignOrRemovePolicyId(
action: 'assign' | 'remove',
policyId: string,
trustedApps: MaybeImmutable<TrustedApp[]>
): Promise<PutTrustedAppUpdateResponse[]> {
if (policyId.trim() === '') {
throw new Error('policy ID is required');
}
if (trustedApps.length === 0) {
throw new Error('at least one trusted app is required');
}
return pMap(
trustedApps,
async (trustedApp) => {
if (isGlobalEffectScope(trustedApp.effectScope)) {
throw new Error(
`Unable to update trusted app [${trustedApp.id}] policy assignment. It's effectScope is 'global'`
);
}
const policies: string[] = !isGlobalEffectScope(trustedApp.effectScope)
? [...trustedApp.effectScope.policies]
: [];
const indexOfPolicyId = policies.indexOf(policyId);
if (action === 'assign' && indexOfPolicyId === -1) {
policies.push(policyId);
} else if (action === 'remove' && indexOfPolicyId !== -1) {
policies.splice(indexOfPolicyId, 1);
}
return this.updateTrustedApp(
{ id: trustedApp.id },
{
...toUpdateTrustedApp(trustedApp),
effectScope: {
type: 'policy',
policies,
},
}
);
},
P_MAP_OPTIONS
);
}
}

View file

@ -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 type { HttpStart } from 'kibana/public';
import {
PostTrustedAppCreateRequest,
PutTrustedAppUpdateRequest,
} from '../../../../../common/endpoint/types';
import { HttpRequestValidationError } from './errors';
import { sendGetAgentPolicyList } from '../../policy/store/services/ingest';
import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../fleet/common';
/**
* Validates that the Trusted App is valid for sending to the API (`POST` + 'PUT')
*
* @throws
*/
export const validateTrustedAppHttpRequestBody = async (
http: HttpStart,
trustedApp: PostTrustedAppCreateRequest | PutTrustedAppUpdateRequest
): Promise<void> => {
const failedValidations: string[] = [];
// Validate that the Policy IDs are still valid
if (trustedApp.effectScope.type === 'policy' && trustedApp.effectScope.policies.length) {
const policyIds = trustedApp.effectScope.policies;
// We can't search against the Package Policy API by ID because there is no way to do that.
// So, as a work-around, we use the Agent Policy API and check for those Agent Policies that
// have these package policies in it. For endpoint, these are 1-to-1.
const agentPoliciesFound = await sendGetAgentPolicyList(http, {
query: {
kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies: (${policyIds.join(' or ')})`,
},
});
if (!agentPoliciesFound.items.length) {
failedValidations.push(`Invalid Policy Id(s) [${policyIds.join(', ')}]`);
} else {
const missingPolicies = policyIds.filter(
(policyId) =>
!agentPoliciesFound.items.find(({ package_policies: packagePolicies }) =>
(packagePolicies as string[]).includes(policyId)
)
);
if (missingPolicies.length) {
failedValidations.push(`Invalid Policy Id(s) [${missingPolicies.join(', ')}]`);
}
}
}
if (failedValidations.length) {
throw new HttpRequestValidationError(failedValidations);
}
};

View file

@ -13,28 +13,18 @@ import { fireEvent } from '@testing-library/dom';
import { MiddlewareActionSpyHelper } from '../../../../common/store/test_utils';
import {
ConditionEntryField,
GetTrustedAppsListResponse,
NewTrustedApp,
OperatingSystem,
PostTrustedAppCreateResponse,
TrustedApp,
} from '../../../../../common/endpoint/types';
import { HttpFetchOptions } from 'kibana/public';
import {
TRUSTED_APPS_GET_API,
TRUSTED_APPS_LIST_API,
} from '../../../../../common/endpoint/constants';
import {
GetPackagePoliciesResponse,
PACKAGE_POLICY_API_ROUTES,
} from '../../../../../../fleet/common';
import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
import { HttpFetchOptions, HttpFetchOptionsWithPath } from 'kibana/public';
import { isFailedResourceState, isLoadedResourceState } from '../state';
import { forceHTMLElementOffsetWidth } from './components/effected_policy_select/test_utils';
import { toUpdateTrustedApp } from '../../../../../common/endpoint/service/trusted_apps/to_update_trusted_app';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables';
import { licenseService } from '../../../../common/hooks/use_license';
import { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants';
import { trustedAppsAllHttpMocks } from '../../mocks';
// TODO: remove this mock when feature flag is removed
jest.mock('../../../../common/hooks/use_experimental_features');
@ -59,66 +49,17 @@ describe('When on the Trusted Apps Page', () => {
'Add a trusted application to improve performance or alleviate conflicts with other ' +
'applications running on your hosts.';
const generator = new EndpointDocGenerator('policy-list');
let mockedContext: AppContextTestRender;
let history: AppContextTestRender['history'];
let coreStart: AppContextTestRender['coreStart'];
let waitForAction: MiddlewareActionSpyHelper['waitForAction'];
let render: () => ReturnType<AppContextTestRender['render']>;
let mockedApis: ReturnType<typeof trustedAppsAllHttpMocks>;
const originalScrollTo = window.scrollTo;
const act = reactTestingLibrary.act;
const getFakeTrustedApp = jest.fn();
const createListApiResponse = (
page: number = 1,
// eslint-disable-next-line @typescript-eslint/naming-convention
per_page: number = 20
): GetTrustedAppsListResponse => {
return {
data: [getFakeTrustedApp()],
total: 50, // << Should be a value large enough to fulfill two pages
page,
per_page,
};
};
const mockListApis = (http: AppContextTestRender['coreStart']['http']) => {
const currentGetHandler = http.get.getMockImplementation();
http.get.mockImplementation(async (...args) => {
const path = args[0] as unknown as string;
// @ts-expect-error TS2352
const httpOptions = args[1] as HttpFetchOptions;
if (path === TRUSTED_APPS_LIST_API) {
return createListApiResponse(
Number(httpOptions?.query?.page ?? 1),
Number(httpOptions?.query?.per_page ?? 20)
);
}
if (path === PACKAGE_POLICY_API_ROUTES.LIST_PATTERN) {
const policy = generator.generatePolicyPackagePolicy();
policy.name = 'test policy A';
policy.id = 'abc123';
const response: GetPackagePoliciesResponse = {
items: [policy],
page: 1,
perPage: 1000,
total: 1,
};
return response;
}
if (currentGetHandler) {
return currentGetHandler(...args);
}
});
};
beforeAll(() => {
window.scrollTo = () => {};
});
@ -131,15 +72,15 @@ describe('When on the Trusted Apps Page', () => {
mockedContext = createAppRootMockRenderer();
getFakeTrustedApp.mockImplementation(
(): TrustedApp => ({
id: '1111-2222-3333-4444',
id: '2d95bec3-b48f-4db7-9622-a2b061cc031d',
version: 'abc123',
name: 'one app',
name: 'Generated Exception (3xnng)',
os: OperatingSystem.WINDOWS,
created_at: '2021-01-04T13:55:00.561Z',
created_by: 'me',
updated_at: '2021-01-04T13:55:00.561Z',
updated_by: 'me',
description: 'a good one',
description: 'created by ExceptionListItemGenerator',
effectScope: { type: 'global' },
entries: [
{
@ -156,6 +97,7 @@ describe('When on the Trusted Apps Page', () => {
coreStart = mockedContext.coreStart;
(licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true);
waitForAction = mockedContext.middlewareSpy.waitForAction;
mockedApis = trustedAppsAllHttpMocks(coreStart.http);
render = () => mockedContext.render(<TrustedAppsPage />);
reactTestingLibrary.act(() => {
history.push('/administration/trusted_apps');
@ -174,8 +116,6 @@ describe('When on the Trusted Apps Page', () => {
return renderResult;
};
beforeEach(() => mockListApis(coreStart.http));
it('should display subtitle info about trusted apps', async () => {
const { getByTestId } = await renderWithListData();
expect(getByTestId('header-panel-subtitle').textContent).toEqual(expectedAboutInfo);
@ -199,7 +139,8 @@ describe('When on the Trusted Apps Page', () => {
renderResult = await renderWithListData();
await act(async () => {
(await renderResult.findAllByTestId('trustedAppCard-header-actions-button'))[0].click();
// The 3rd Trusted app to be rendered will be a policy specific one
(await renderResult.findAllByTestId('trustedAppCard-header-actions-button'))[2].click();
});
act(() => {
@ -284,7 +225,9 @@ describe('When on the Trusted Apps Page', () => {
});
it('should persist edit params to url', () => {
expect(history.location.search).toEqual('?show=edit&id=1111-2222-3333-4444');
expect(history.location.search).toEqual(
'?show=edit&id=2d95bec3-b48f-4db7-9622-a2b061cc031d'
);
});
it('should display the Edit flyout', () => {
@ -315,14 +258,19 @@ describe('When on the Trusted Apps Page', () => {
'addTrustedAppFlyout-createForm-descriptionField'
) as HTMLTextAreaElement;
expect(formNameInput.value).toEqual('one app');
expect(formDescriptionInput.value).toEqual('a good one');
expect(formNameInput.value).toEqual('Generated Exception (3xnng)');
expect(formDescriptionInput.value).toEqual('created by ExceptionListItemGenerator');
});
describe('and when Save is clicked', () => {
it('should call the correct api (PUT)', () => {
act(() => {
it('should call the correct api (PUT)', async () => {
await act(async () => {
fireEvent.click(renderResult.getByTestId('addTrustedAppFlyout-createButton'));
await waitForAction('trustedAppCreationSubmissionResourceStateChanged', {
validate({ payload }) {
return isLoadedResourceState(payload.newState);
},
});
});
expect(coreStart.http.put).toHaveBeenCalledTimes(1);
@ -332,33 +280,43 @@ describe('When on the Trusted Apps Page', () => {
HttpFetchOptions
];
expect(lastCallToPut[0]).toEqual('/api/endpoint/trusted_apps/1111-2222-3333-4444');
expect(lastCallToPut[0]).toEqual('/api/exception_lists/items');
expect(JSON.parse(lastCallToPut[1].body as string)).toEqual({
name: 'one app',
os: 'windows',
_version: '3o9za',
name: 'Generated Exception (3xnng)',
description: 'created by ExceptionListItemGenerator',
entries: [
{
field: 'process.executable.caseless',
value: 'one/two',
field: 'process.hash.md5',
operator: 'included',
type: 'match',
value: '1234234659af249ddf3e40864e9fb241',
},
{
field: 'process.executable.caseless',
operator: 'included',
type: 'match',
value: '/one/two/three',
},
],
description: 'a good one',
effectScope: {
type: 'global',
},
version: 'abc123',
os_types: ['windows'],
tags: [
'policy:ddf6570b-9175-4a6d-b288-61a09771c647',
'policy:b8e616ae-44fc-4be7-846c-ce8fa5c082dd',
],
id: '05b5e350-0cad-4dc3-a61d-6e6796b0af39',
comments: [],
item_id: '2d95bec3-b48f-4db7-9622-a2b061cc031d',
meta: {},
namespace_type: 'agnostic',
type: 'simple',
});
});
});
});
describe('and attempting to show Edit panel based on URL params', () => {
const TRUSTED_APP_GET_URI = resolvePathVariables(TRUSTED_APPS_GET_API, {
id: '9999-edit-8888',
});
const renderAndWaitForGetApi = async () => {
// the store action watcher is setup prior to render because `renderWithListData()`
// also awaits API calls and this action could be missed.
@ -381,23 +339,6 @@ describe('When on the Trusted Apps Page', () => {
};
beforeEach(() => {
// Mock the API GET for the trusted application
const priorMockImplementation = coreStart.http.get.getMockImplementation();
coreStart.http.get.mockImplementation(async (...args) => {
if ('string' === typeof args[0] && args[0] === TRUSTED_APP_GET_URI) {
return {
data: {
...getFakeTrustedApp(),
id: '9999-edit-8888',
name: 'one app for edit',
},
};
}
if (priorMockImplementation) {
return priorMockImplementation(...args);
}
});
reactTestingLibrary.act(() => {
history.push('/administration/trusted_apps?show=edit&id=9999-edit-8888');
});
@ -406,7 +347,15 @@ describe('When on the Trusted Apps Page', () => {
it('should retrieve trusted app via API using url `id`', async () => {
renderResult = await renderAndWaitForGetApi();
expect(coreStart.http.get).toHaveBeenCalledWith(TRUSTED_APP_GET_URI);
expect(coreStart.http.get.mock.calls).toContainEqual([
EXCEPTION_LIST_ITEM_URL,
{
query: {
item_id: '9999-edit-8888',
namespace_type: 'agnostic',
},
},
]);
expect(
(
@ -414,7 +363,7 @@ describe('When on the Trusted Apps Page', () => {
'addTrustedAppFlyout-createForm-nameTextField'
) as HTMLInputElement
).value
).toEqual('one app for edit');
).toEqual('Generated Exception (u6kh2)');
});
it('should redirect to list and show toast message if `id` is missing from URL', async () => {
@ -432,14 +381,8 @@ describe('When on the Trusted Apps Page', () => {
it('should redirect to list and show toast message on API error for GET of `id`', async () => {
// Mock the API GET for the trusted application
const priorMockImplementation = coreStart.http.get.getMockImplementation();
coreStart.http.get.mockImplementation(async (...args) => {
if ('string' === typeof args[0] && args[0] === TRUSTED_APP_GET_URI) {
throw new Error('test: api error response');
}
if (priorMockImplementation) {
return priorMockImplementation(...args);
}
mockedApis.responseProvider.trustedApp.mockImplementation(() => {
throw new Error('test: api error response');
});
await renderAndWaitForGetApi();
@ -486,8 +429,6 @@ describe('When on the Trusted Apps Page', () => {
return renderResult;
};
beforeEach(() => mockListApis(coreStart.http));
it('should display the create flyout', async () => {
const { getByTestId } = await renderAndClickAddButton();
const flyout = getByTestId('addTrustedAppFlyout');
@ -505,6 +446,14 @@ describe('When on the Trusted Apps Page', () => {
});
it('should preserve other URL search params', async () => {
const createListResponse =
mockedApis.responseProvider.trustedAppsList.getMockImplementation()!;
mockedApis.responseProvider.trustedAppsList.mockImplementation((...args) => {
const response = createListResponse(...args);
response.total = 100; // Trigger the UI to show pagination
return response;
});
reactTestingLibrary.act(() => {
history.push('/administration/trusted_apps?page_index=2&page_size=20');
});
@ -524,7 +473,7 @@ describe('When on the Trusted Apps Page', () => {
act(() => {
fireEvent.click(renderResult.getByTestId('perPolicy'));
});
expect(renderResult.getByTestId('policy-abc123'));
expect(renderResult.getByTestId('policy-ddf6570b-9175-4a6d-b288-61a09771c647'));
resetEnv();
});
@ -582,39 +531,33 @@ describe('When on the Trusted Apps Page', () => {
describe('and the Flyout Add button is clicked', () => {
let renderResult: ReturnType<AppContextTestRender['render']>;
let resolveHttpPost: (response?: PostTrustedAppCreateResponse) => void;
let httpPostBody: string;
let rejectHttpPost: (response: Error) => void;
let releasePostCreateApi: () => void;
beforeEach(async () => {
// Mock the http.post() call and expose `resolveHttpPost()` method so that
// we can control when the API call response is returned, which will allow us
// to test the UI behaviours while the API call is in flight
coreStart.http.post.mockImplementation(
// @ts-expect-error TS2345
async (_, options: HttpFetchOptions) => {
return new Promise((resolve, reject) => {
httpPostBody = options.body as string;
resolveHttpPost = resolve;
rejectHttpPost = reject;
});
}
// Add a delay to the create api response provider and expose a function that allows
// us to release it at the right time.
mockedApis.responseProvider.trustedAppCreate.mockDelay.mockReturnValue(
new Promise((resolve) => {
releasePostCreateApi = resolve as typeof releasePostCreateApi;
})
);
renderResult = await renderAndClickAddButton();
await fillInCreateForm();
const userClickedSaveActionWatcher = waitForAction('trustedAppCreationDialogConfirmed');
reactTestingLibrary.act(() => {
fireEvent.click(renderResult.getByTestId('addTrustedAppFlyout-createButton'), {
button: 1,
});
});
await reactTestingLibrary.act(async () => {
await userClickedSaveActionWatcher;
});
});
afterEach(() => resolveHttpPost());
afterEach(() => releasePostCreateApi());
it('should display info about Trusted Apps', async () => {
expect(renderResult.getByTestId('addTrustedAppFlyout-about').textContent).toEqual(
@ -627,7 +570,7 @@ describe('When on the Trusted Apps Page', () => {
(renderResult.getByTestId('addTrustedAppFlyout-cancelButton') as HTMLButtonElement)
.disabled
).toBe(true);
resolveHttpPost();
releasePostCreateApi();
});
it('should hide the dialog close button', async () => {
@ -644,23 +587,13 @@ describe('When on the Trusted Apps Page', () => {
describe('and if create was successful', () => {
beforeEach(async () => {
const successCreateApiResponse: PostTrustedAppCreateResponse = {
data: {
...(JSON.parse(httpPostBody) as NewTrustedApp),
id: '1',
version: 'abc123',
created_at: '2020-09-16T14:09:45.484Z',
created_by: 'kibana',
updated_at: '2021-01-04T13:55:00.561Z',
updated_by: 'me',
},
};
await reactTestingLibrary.act(async () => {
const serverResponseAction = waitForAction(
'trustedAppCreationSubmissionResourceStateChanged'
);
coreStart.http.get.mockClear();
resolveHttpPost(successCreateApiResponse);
releasePostCreateApi();
await serverResponseAction;
});
});
@ -671,33 +604,47 @@ describe('When on the Trusted Apps Page', () => {
it('should show success toast notification', () => {
expect(coreStart.notifications.toasts.addSuccess.mock.calls[0][0]).toEqual({
text: '"one app" has been added to the Trusted Applications list.',
text: '"Generated Exception (3xnng)" has been added to the Trusted Applications list.',
title: 'Success!',
});
});
it('should trigger the List to reload', () => {
const isCalled = coreStart.http.get.mock.calls.some(
(call) => call[0].toString() === TRUSTED_APPS_LIST_API
(call) => call[0].toString() === `${EXCEPTION_LIST_ITEM_URL}/_find`
);
expect(isCalled).toEqual(true);
});
});
describe('and if create failed', () => {
const ServerErrorResponseBodyMock = class extends Error {
public readonly body: { message: string };
constructor(message = 'Test - Bad Call') {
super(message);
this.body = {
message,
};
}
};
beforeEach(async () => {
const failedCreateApiResponse: Error & { body?: { message: string } } = new Error(
'Bad call'
);
failedCreateApiResponse.body = {
message: 'bad call',
};
const failedCreateApiResponse = new ServerErrorResponseBodyMock();
mockedApis.responseProvider.trustedAppCreate.mockImplementation(() => {
throw failedCreateApiResponse;
});
await reactTestingLibrary.act(async () => {
const serverResponseAction = waitForAction(
'trustedAppCreationSubmissionResourceStateChanged'
'trustedAppCreationSubmissionResourceStateChanged',
{
validate({ payload }) {
return isFailedResourceState(payload.newState);
},
}
);
coreStart.http.get.mockClear();
rejectHttpPost(failedCreateApiResponse);
releasePostCreateApi();
await serverResponseAction;
});
});
@ -773,54 +720,31 @@ describe('When on the Trusted Apps Page', () => {
});
describe('and there are no trusted apps', () => {
const releaseExistsResponse: jest.MockedFunction<() => Promise<GetTrustedAppsListResponse>> =
jest.fn(async () => {
return {
data: [],
total: 0,
page: 1,
per_page: 1,
};
});
const releaseListResponse: jest.MockedFunction<() => Promise<GetTrustedAppsListResponse>> =
jest.fn(async () => {
return {
data: [],
total: 0,
page: 1,
per_page: 20,
};
});
const releaseExistsResponse = jest.fn((): FoundExceptionListItemSchema => {
return {
data: [],
total: 0,
page: 1,
per_page: 1,
};
});
const releaseListResponse = jest.fn((): FoundExceptionListItemSchema => {
return {
data: [],
total: 0,
page: 1,
per_page: 20,
};
});
beforeEach(() => {
const priorMockImplementation = coreStart.http.get.getMockImplementation();
// @ts-expect-error TS7006
coreStart.http.get.mockImplementation((path, options) => {
if (path === TRUSTED_APPS_LIST_API) {
const { page, per_page: perPage } = options.query as { page: number; per_page: number };
mockedApis.responseProvider.trustedAppsList.mockImplementation(({ query }) => {
const { page, per_page: perPage } = query as { page: number; per_page: number };
if (page === 1 && perPage === 1) {
return releaseExistsResponse();
} else {
return releaseListResponse();
}
}
if (path === PACKAGE_POLICY_API_ROUTES.LIST_PATTERN) {
const policy = generator.generatePolicyPackagePolicy();
policy.name = 'test policy A';
policy.id = 'abc123';
const response: GetPackagePoliciesResponse = {
items: [policy],
page: 1,
perPage: 1000,
total: 1,
};
return response;
}
if (priorMockImplementation) {
return priorMockImplementation(path);
if (page === 1 && perPage === 1) {
return releaseExistsResponse();
} else {
return releaseListResponse();
}
});
});
@ -831,8 +755,6 @@ describe('When on the Trusted Apps Page', () => {
});
it('should show a loader until trusted apps existence can be confirmed', async () => {
// Make the call that checks if Trusted Apps exists not respond back
releaseExistsResponse.mockImplementationOnce(() => new Promise(() => {}));
const renderResult = render();
expect(await renderResult.findByTestId('trustedAppsListLoader')).not.toBeNull();
});
@ -851,14 +773,14 @@ describe('When on the Trusted Apps Page', () => {
await waitForAction('trustedAppsExistStateChanged');
});
expect(await renderResult.findByTestId('trustedAppEmptyState')).not.toBeNull();
releaseListResponse.mockResolvedValueOnce({
data: [getFakeTrustedApp()],
releaseListResponse.mockReturnValueOnce({
data: [mockedApis.responseProvider.trustedApp({ query: {} } as HttpFetchOptionsWithPath)],
total: 1,
page: 1,
per_page: 20,
});
releaseExistsResponse.mockResolvedValueOnce({
data: [getFakeTrustedApp()],
releaseExistsResponse.mockReturnValueOnce({
data: [mockedApis.responseProvider.trustedApp({ query: {} } as HttpFetchOptionsWithPath)],
total: 1,
page: 1,
per_page: 1,
@ -875,14 +797,14 @@ describe('When on the Trusted Apps Page', () => {
});
it('should should show empty prompt once the last trusted app entry is deleted', async () => {
releaseListResponse.mockResolvedValueOnce({
data: [getFakeTrustedApp()],
releaseListResponse.mockReturnValueOnce({
data: [mockedApis.responseProvider.trustedApp({ query: {} } as HttpFetchOptionsWithPath)],
total: 1,
page: 1,
per_page: 20,
});
releaseExistsResponse.mockResolvedValueOnce({
data: [getFakeTrustedApp()],
releaseExistsResponse.mockReturnValueOnce({
data: [mockedApis.responseProvider.trustedApp({ query: {} } as HttpFetchOptionsWithPath)],
total: 1,
page: 1,
per_page: 1,
@ -896,19 +818,6 @@ describe('When on the Trusted Apps Page', () => {
expect(await renderResult.findByTestId('trustedAppsListPageContent')).not.toBeNull();
releaseListResponse.mockResolvedValueOnce({
data: [],
total: 0,
page: 1,
per_page: 20,
});
releaseExistsResponse.mockResolvedValueOnce({
data: [],
total: 0,
page: 1,
per_page: 1,
});
await act(async () => {
mockedContext.store.dispatch({
type: 'trustedAppsListDataOutdated',
@ -931,7 +840,6 @@ describe('When on the Trusted Apps Page', () => {
describe('and the search is dispatched', () => {
let renderResult: ReturnType<AppContextTestRender['render']>;
beforeEach(async () => {
mockListApis(coreStart.http);
reactTestingLibrary.act(() => {
history.push('/administration/trusted_apps?filter=test');
});
@ -956,28 +864,6 @@ describe('When on the Trusted Apps Page', () => {
describe('and the back button is present', () => {
let renderResult: ReturnType<AppContextTestRender['render']>;
beforeEach(async () => {
// Ensure implementation is defined before render to avoid undefined responses from hidden api calls
const priorMockImplementation = coreStart.http.get.getMockImplementation();
// @ts-expect-error TS7006
coreStart.http.get.mockImplementation((path, options) => {
if (path === PACKAGE_POLICY_API_ROUTES.LIST_PATTERN) {
const policy = generator.generatePolicyPackagePolicy();
policy.name = 'test policy A';
policy.id = 'abc123';
const response: GetPackagePoliciesResponse = {
items: [policy],
page: 1,
perPage: 1000,
total: 1,
};
return response;
}
if (priorMockImplementation) {
return priorMockImplementation(path);
}
});
renderResult = render();
await act(async () => {
await waitForAction('trustedAppsListResourceStateChanged');

View file

@ -7,13 +7,7 @@
/* eslint-disable max-classes-per-file */
export class EndpointError extends Error {
constructor(message: string, public readonly meta?: unknown) {
super(message);
// For debugging - capture name of subclasses
this.name = this.constructor.name;
}
}
import { EndpointError } from '../../common/endpoint/errors';
export class NotFoundError extends EndpointError {}

View file

@ -18,7 +18,7 @@ import { isEmptyManifestDiff, Manifest } from './manifest';
import { InvalidInternalManifestError } from '../../services/artifacts/errors';
import { ManifestManager } from '../../services';
import { wrapErrorIfNeeded } from '../../utils';
import { EndpointError } from '../../errors';
import { EndpointError } from '../../../../common/endpoint/errors';
export const ManifestTaskConstants = {
TIMEOUT: '1m',

View file

@ -34,7 +34,8 @@ import { findAgentIdsByStatus } from './support/agent_status';
import { EndpointAppContextService } from '../../endpoint_app_context_services';
import { fleetAgentStatusToEndpointHostStatus } from '../../utils';
import { queryResponseToHostListResult } from './support/query_strategies';
import { EndpointError, NotFoundError } from '../../errors';
import { NotFoundError } from '../../errors';
import { EndpointError } from '../../../../common/endpoint/errors';
import { EndpointHostUnEnrolledError } from '../../services/metadata';
import { CustomHttpRequestError } from '../../../utils/custom_http_request_error';
import { GetMetadataListRequestQuery } from '../../../../common/endpoint/schema/metadata';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EndpointError } from '../../errors';
import { EndpointError } from '../../../../common/endpoint/errors';
/**
* Indicates that the internal manifest that is managed by ManifestManager is invalid or contains

View file

@ -38,8 +38,8 @@ import {
import { ManifestManager } from './manifest_manager';
import { EndpointArtifactClientInterface } from '../artifact_client';
import { EndpointError } from '../../../errors';
import { InvalidInternalManifestError } from '../errors';
import { EndpointError } from '../../../../../common/endpoint/errors';
const getArtifactObject = (artifact: InternalArtifactSchema) =>
JSON.parse(Buffer.from(artifact.body!, 'base64').toString());

View file

@ -38,7 +38,7 @@ import { ManifestClient } from '../manifest_client';
import { ExperimentalFeatures } from '../../../../../common/experimental_features';
import { InvalidInternalManifestError } from '../errors';
import { wrapErrorIfNeeded } from '../../../utils';
import { EndpointError } from '../../../errors';
import { EndpointError } from '../../../../../common/endpoint/errors';
interface ArtifactsBuildResult {
defaultArtifacts: InternalArtifactCompleteSchema[];

View file

@ -21,10 +21,10 @@ import {
getESQueryHostMetadataByFleetAgentIds,
buildUnitedIndexQuery,
} from '../../routes/metadata/query_builders';
import { EndpointError } from '../../errors';
import { HostMetadata } from '../../../../common/endpoint/types';
import { Agent } from '../../../../../fleet/common';
import { AgentPolicyServiceInterface } from '../../../../../fleet/server/services';
import { EndpointError } from '../../../../common/endpoint/errors';
describe('EndpointMetadataService', () => {
let testMockedContext: EndpointMetadataServiceTestContextMock;

View file

@ -51,12 +51,12 @@ import {
fleetAgentStatusToEndpointHostStatus,
wrapErrorIfNeeded,
} from '../../utils';
import { EndpointError } from '../../errors';
import { createInternalReadonlySoClient } from '../../utils/create_internal_readonly_so_client';
import { METADATA_UNITED_INDEX } from '../../../../common/endpoint/constants';
import { getAllEndpointPackagePolicies } from '../../routes/metadata/support/endpoint_package_policies';
import { getAgentStatus } from '../../../../../fleet/common/services/agent_status';
import { GetMetadataListRequestQuery } from '../../../../common/endpoint/schema/metadata';
import { EndpointError } from '../../../../common/endpoint/errors';
type AgentPolicyWithPackagePolicies = Omit<AgentPolicy, 'package_policies'> & {
package_policies: PackagePolicy[];

View file

@ -7,7 +7,8 @@
/* eslint-disable max-classes-per-file */
import { EndpointError, NotFoundError } from '../../errors';
import { NotFoundError } from '../../errors';
import { EndpointError } from '../../../../common/endpoint/errors';
export class EndpointHostNotFoundError extends NotFoundError {}

View file

@ -6,7 +6,7 @@
*/
import { KibanaRequest, SavedObjectsClientContract, SavedObjectsServiceStart } from 'kibana/server';
import { EndpointError } from '../errors';
import { EndpointError } from '../../../common/endpoint/errors';
type SavedObjectsClientContractKeys = keyof SavedObjectsClientContract;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EndpointError } from '../errors';
import { EndpointError } from '../../../common/endpoint/errors';
/**
* Will wrap the given Error with `EndpointError`, which will help getting a good picture of where in

View file

@ -54,4 +54,3 @@ export type { ConfigType, PluginSetup, PluginStart };
export { Plugin };
export { AppClient };
export type { SecuritySolutionApiRequestHandlerContext } from './types';
export { EndpointError } from './endpoint/errors';

View file

@ -12,7 +12,6 @@ import {
metadataCurrentIndexPattern,
metadataTransformPrefix,
} from '../../../plugins/security_solution/common/endpoint/constants';
import { EndpointError } from '../../../plugins/security_solution/server';
import {
deleteIndexedHostsAndAlerts,
IndexedHostsAndAlertsResponse,
@ -22,6 +21,7 @@ import { TransformConfigUnion } from '../../../plugins/transform/common/types/tr
import { GetTransformsResponseSchema } from '../../../plugins/transform/common/api_schemas/transforms';
import { catchAndWrapError } from '../../../plugins/security_solution/server/endpoint/utils';
import { installOrUpgradeEndpointFleetPackage } from '../../../plugins/security_solution/common/endpoint/data_loaders/setup_fleet_for_endpoint';
import { EndpointError } from '../../../plugins/security_solution/common/endpoint/errors';
export class EndpointTestResources extends FtrService {
private readonly esClient = this.ctx.getService('es');

View file

@ -24,7 +24,7 @@ import { Immutable } from '../../../plugins/security_solution/common/endpoint/ty
// NOTE: import path below should be the deep path to the actual module - else we get CI errors
import { pkgKeyFromPackageInfo } from '../../../plugins/fleet/public/services/pkg_key_from_package_info';
import { EndpointError } from '../../../plugins/security_solution/server';
import { EndpointError } from '../../../plugins/security_solution/common/endpoint/errors';
const INGEST_API_ROOT = '/api/fleet';
const INGEST_API_AGENT_POLICIES = `${INGEST_API_ROOT}/agent_policies`;