[Security Solution][Endpoint] Add Host Isolation Exceptions api validations get, find, delete, export, summary and import (#123954)

* validation for Pre GET one of host isolation exceptions.
* adjust checks for host isolation validation
* Add validation for import for all artifacts
* Validate host isolation exceptions exports
* Validate host isolation exceptions multi list find
* Validate host isolation exceptions single list find
* Validate host isolation exceptions Summary
* add FTR tests to validate authz
* Update all exception extension point handlers to use the ExceptionListClient passed in on context
* Refactored ExceptionListItemGenerator a bit and added methods to get Host Isolation exceptions
* Update handlers to immediately exit if the namespace_type is not `agnostic`
* Improved `log.info` messages in artifact and policy services
* Add `lists-summary` to Security solution `all` feature privilege (was missing)
This commit is contained in:
Paul Tavares 2022-01-31 09:13:25 -05:00 committed by GitHub
parent 940c4e2833
commit 997988fac2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 524 additions and 203 deletions

View file

@ -39,7 +39,8 @@ export type GetListClientType = (
export type GetExceptionListClientType = (
savedObjectsClient: SavedObjectsClientContract,
user: string,
disableServerExtensionPoints?: boolean
/** Default is `true` - processing of server extension points are always on by default */
enableServerExtensionPoints?: boolean
) => ExceptionListClient;
export interface ListPluginSetup {

View file

@ -13,6 +13,7 @@ import type {
import {
ENDPOINT_EVENT_FILTERS_LIST_ID,
ENDPOINT_TRUSTED_APPS_LIST_ID,
ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID,
} from '@kbn/securitysolution-list-constants';
import { BaseDataGenerator } from './base_data_generator';
import { ConditionEntryField } from '../types';
@ -37,6 +38,55 @@ type UpdateExceptionListItemSchemaWithNonNullProps = NonNullableTypeProperties<
> &
Pick<UpdateExceptionListItemSchema, 'meta'>;
const exceptionItemToCreateExceptionItem = (
exceptionItem: ExceptionListItemSchema
): 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 */
} = exceptionItem;
return {
description,
entries,
list_id,
name,
type,
comments,
item_id,
meta,
namespace_type,
os_types,
tags,
};
};
const exceptionItemToUpdateExceptionItem = (
exceptionItem: ExceptionListItemSchema
): UpdateExceptionListItemSchemaWithNonNullProps => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { id, item_id, _version } = exceptionItem;
const { list_id: _, ...updateAttributes } = exceptionItemToCreateExceptionItem(exceptionItem);
return {
...updateAttributes,
id,
item_id,
_version: _version ?? 'some value',
};
};
export class ExceptionsListItemGenerator extends BaseDataGenerator<ExceptionListItemSchema> {
generate(overrides: Partial<ExceptionListItemSchema> = {}): ExceptionListItemSchema {
const exceptionItem: ExceptionListItemSchema = {
@ -85,82 +135,22 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator<ExceptionList
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,
};
return Object.assign(exceptionItemToCreateExceptionItem(this.generate()), overrides);
}
generateTrustedApp(overrides: Partial<ExceptionListItemSchema> = {}): ExceptionListItemSchema {
const trustedApp = this.generate(overrides);
return {
...trustedApp,
return this.generate({
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),
};
...overrides,
});
}
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,
...exceptionItemToCreateExceptionItem(this.generateTrustedApp()),
...overrides,
};
}
@ -168,36 +158,8 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator<ExceptionList
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',
...exceptionItemToUpdateExceptionItem(this.generateTrustedApp()),
...overrides,
};
}
@ -215,33 +177,8 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator<ExceptionList
generateEventFilterForCreate(
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.generateEventFilter();
return {
description,
entries,
list_id,
name,
type,
comments,
item_id,
meta,
namespace_type,
os_types,
tags,
...exceptionItemToCreateExceptionItem(this.generateEventFilter()),
...overrides,
};
}
@ -249,35 +186,27 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator<ExceptionList
generateEventFilterForUpdate(
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.generateEventFilter();
return {
description,
entries,
name,
type,
comments,
id,
item_id,
meta,
namespace_type,
os_types,
tags,
_version: _version ?? 'some value',
...exceptionItemToUpdateExceptionItem(this.generateEventFilter()),
...overrides,
};
}
generateHostIsolationException(
overrides: Partial<ExceptionListItemSchema> = {}
): ExceptionListItemSchema {
return this.generate({
name: `Host Isolation (${this.randomString(5)})`,
list_id: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID,
...overrides,
});
}
generateHostIsolationExceptionForCreate(
overrides: Partial<CreateExceptionListItemSchema> = {}
): CreateExceptionListItemSchemaWithNonNullProps {
return {
...exceptionItemToCreateExceptionItem(this.generateHostIsolationException()),
...overrides,
};
}

View file

@ -5,6 +5,18 @@
* 2.0.
*/
import {
ENDPOINT_TRUSTED_APPS_LIST_ID,
ENDPOINT_EVENT_FILTERS_LIST_ID,
ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID,
} from '@kbn/securitysolution-list-constants';
export const BY_POLICY_ARTIFACT_TAG_PREFIX = 'policy:';
export const GLOBAL_ARTIFACT_TAG = `${BY_POLICY_ARTIFACT_TAG_PREFIX}all`;
export const ALL_ENDPOINT_ARTIFACT_LIST_IDS: readonly string[] = [
ENDPOINT_TRUSTED_APPS_LIST_ID,
ENDPOINT_EVENT_FILTERS_LIST_ID,
ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID,
];

View file

@ -4,7 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ExceptionListType, ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import {
ExceptionListType,
ExceptionListTypeEnum,
CreateExceptionListSchema,
} from '@kbn/securitysolution-io-ts-list-types';
import {
ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_DESCRIPTION,
ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID,
@ -14,7 +18,7 @@ import {
export const HOST_ISOLATION_EXCEPTIONS_LIST_TYPE: ExceptionListType =
ExceptionListTypeEnum.ENDPOINT_HOST_ISOLATION_EXCEPTIONS;
export const HOST_ISOLATION_EXCEPTIONS_LIST = {
export const HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION: CreateExceptionListSchema = {
name: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_NAME,
namespace_type: 'agnostic',
description: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_DESCRIPTION,

View file

@ -16,12 +16,12 @@ import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolutio
import { transformNewItemOutput, transformOutput } from '@kbn/securitysolution-list-hooks';
import { HttpStart } from 'kibana/public';
import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../event_filters/constants';
import { HOST_ISOLATION_EXCEPTIONS_LIST } from './constants';
import { HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION } from './constants';
async function createHostIsolationExceptionList(http: HttpStart): Promise<void> {
try {
await http.post<ExceptionListItemSchema>(EXCEPTION_LIST_URL, {
body: JSON.stringify(HOST_ISOLATION_EXCEPTIONS_LIST),
body: JSON.stringify(HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION),
});
} catch (err) {
// Ignore 409 errors. List already created

View file

@ -118,7 +118,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (ruleTypes: string[]): Kiban
all: {
app: [APP_ID, 'kibana'],
catalogue: [APP_ID],
api: [APP_ID, 'lists-all', 'lists-read', 'rac'],
api: [APP_ID, 'lists-all', 'lists-read', 'lists-summary', 'rac'],
savedObject: {
all: [
'alert',

View file

@ -5,14 +5,37 @@
* 2.0.
*/
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import { ExtensionPoint } from '../../../../../lists/server';
import { ExceptionsListPreDeleteItemServerExtension } from '../../../../../lists/server';
import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
export const getExceptionsPreDeleteItemHandler = (
endpointAppContext: EndpointAppContextService
): (ExtensionPoint & { type: 'exceptionsListPreDeleteItem' })['callback'] => {
return async function ({ data }) {
// Individual validators here
): ExceptionsListPreDeleteItemServerExtension['callback'] => {
return async function ({ data, context: { request, exceptionListClient } }) {
if (data.namespaceType !== 'agnostic') {
return data;
}
const exceptionItem: ExceptionListItemSchema | null =
await exceptionListClient.getExceptionListItem({
id: data.id,
itemId: data.itemId,
namespaceType: data.namespaceType,
});
if (!exceptionItem) {
return data;
}
// Host Isolation Exception
if (HostIsolationExceptionsValidator.isHostIsolationException(exceptionItem.list_id)) {
await new HostIsolationExceptionsValidator(
endpointAppContext,
request
).validatePreDeleteItem();
}
return data;
};

View file

@ -6,13 +6,31 @@
*/
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import { ExtensionPoint } from '../../../../../lists/server';
import { ExceptionsListPreExportServerExtension } from '../../../../../lists/server';
import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
export const getExceptionsPreExportHandler = (
endpointAppContext: EndpointAppContextService
): (ExtensionPoint & { type: 'exceptionsListPreExport' })['callback'] => {
return async function ({ data }) {
// Individual validators here
endpointAppContextService: EndpointAppContextService
): ExceptionsListPreExportServerExtension['callback'] => {
return async function ({ data, context: { request, exceptionListClient } }) {
const { listId: maybeListId, id } = data;
let listId: string | null | undefined = maybeListId;
if (!listId && id) {
listId = (await exceptionListClient.getExceptionList(data))?.list_id ?? null;
}
if (!listId) {
return data;
}
// Host Isolation Exceptions validations
if (HostIsolationExceptionsValidator.isHostIsolationException(listId)) {
await new HostIsolationExceptionsValidator(
endpointAppContextService,
request
).validatePreExport();
}
return data;
};

View file

@ -5,14 +5,39 @@
* 2.0.
*/
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import { ExtensionPoint } from '../../../../../lists/server';
import { ExceptionsListPreGetOneItemServerExtension } from '../../../../../lists/server';
import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
export const getExceptionsPreGetOneHandler = (
endpointAppContext: EndpointAppContextService
): (ExtensionPoint & { type: 'exceptionsListPreGetOneItem' })['callback'] => {
return async function ({ data }) {
// Individual validators here
endpointAppContextService: EndpointAppContextService
): ExceptionsListPreGetOneItemServerExtension['callback'] => {
return async function ({ data, context: { request, exceptionListClient } }) {
if (data.namespaceType !== 'agnostic') {
return data;
}
const exceptionItem: ExceptionListItemSchema | null =
await exceptionListClient.getExceptionListItem({
id: data.id,
itemId: data.itemId,
namespaceType: data.namespaceType,
});
if (!exceptionItem) {
return data;
}
// validate Host Isolation Exception
if (HostIsolationExceptionsValidator.isHostIsolationException(exceptionItem.list_id)) {
await new HostIsolationExceptionsValidator(
endpointAppContextService,
request
).validatePreGetOneItem();
return data;
}
return data;
};

View file

@ -0,0 +1,31 @@
/*
* 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 { ExceptionsListPreImportServerExtension } from '../../../../../lists/server';
import { EndpointArtifactExceptionValidationError } from '../validators/errors';
import { ALL_ENDPOINT_ARTIFACT_LIST_IDS } from '../../../../common/endpoint/service/artifacts/constants';
export const getExceptionsPreImportHandler =
(): ExceptionsListPreImportServerExtension['callback'] => {
return async ({ data }) => {
const hasEndpointArtifactListOrListItems = [...data.lists, ...data.items].some((item) => {
if ('list_id' in item) {
return ALL_ENDPOINT_ARTIFACT_LIST_IDS.includes(item.list_id);
}
return false;
});
if (hasEndpointArtifactListOrListItems) {
throw new EndpointArtifactExceptionValidationError(
'Import is not supported for Endpoint artifact exceptions'
);
}
return data;
};
};

View file

@ -6,13 +6,24 @@
*/
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import { ExtensionPoint } from '../../../../../lists/server';
import { ExceptionsListPreMultiListFindServerExtension } from '../../../../../lists/server';
import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
export const getExceptionsPreMultiListFindHandler = (
endpointAppContext: EndpointAppContextService
): (ExtensionPoint & { type: 'exceptionsListPreMultiListFind' })['callback'] => {
return async function ({ data }) {
// Individual validators here
): ExceptionsListPreMultiListFindServerExtension['callback'] => {
return async function ({ data, context: { request } }) {
if (!data.namespaceType.includes('agnostic')) {
return data;
}
// Validate Host Isolation Exceptions
if (data.listId.some(HostIsolationExceptionsValidator.isHostIsolationException)) {
await new HostIsolationExceptionsValidator(
endpointAppContext,
request
).validatePreMultiListFind();
}
return data;
};

View file

@ -6,13 +6,24 @@
*/
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import { ExtensionPoint } from '../../../../../lists/server';
import { ExceptionsListPreSingleListFindServerExtension } from '../../../../../lists/server';
import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
export const getExceptionsPreSingleListFindHandler = (
endpointAppContext: EndpointAppContextService
): (ExtensionPoint & { type: 'exceptionsListPreSingleListFind' })['callback'] => {
return async function ({ data }) {
// Individual validators here
): ExceptionsListPreSingleListFindServerExtension['callback'] => {
return async function ({ data, context: { request } }) {
if (data.namespaceType !== 'agnostic') {
return data;
}
// Validate Host Isolation Exceptions
if (HostIsolationExceptionsValidator.isHostIsolationException(data.listId)) {
await new HostIsolationExceptionsValidator(
endpointAppContext,
request
).validatePreSingleListFind();
}
return data;
};

View file

@ -6,13 +6,28 @@
*/
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import { ExtensionPoint } from '../../../../../lists/server';
import { ExceptionsListPreSummaryServerExtension } from '../../../../../lists/server';
import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
export const getExceptionsPreSummaryHandler = (
endpointAppContext: EndpointAppContextService
): (ExtensionPoint & { type: 'exceptionsListPreSummary' })['callback'] => {
return async function ({ data }) {
// Individual validators here
): ExceptionsListPreSummaryServerExtension['callback'] => {
return async function ({ data, context: { request, exceptionListClient } }) {
const { listId: maybeListId, id } = data;
let listId: string | null | undefined = maybeListId;
if (!listId && id) {
listId = (await exceptionListClient.getExceptionList(data))?.list_id ?? null;
}
if (!listId) {
return data;
}
// Host Isolation Exceptions
if (HostIsolationExceptionsValidator.isHostIsolationException(listId)) {
await new HostIsolationExceptionsValidator(endpointAppContext, request).validatePreSummary();
}
return data;
};

View file

@ -15,16 +15,21 @@ import { EventFilterValidator, 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,
});
return async function ({
data,
context: { request, exceptionListClient },
}): Promise<UpdateExceptionListItemOptions> {
if (data.namespaceType !== 'agnostic') {
return data;
}
// We don't want to `throw` here becuase we don't know for sure that the item is one we care about.
const currentSavedItem = await exceptionListClient.getExceptionListItem({
id: data.id,
itemId: data.itemId,
namespaceType: data.namespaceType,
});
// We don't want to `throw` here because 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;

View file

@ -15,6 +15,7 @@ import { getExceptionsPreDeleteItemHandler } from './handlers/exceptions_pre_del
import { getExceptionsPreExportHandler } from './handlers/exceptions_pre_export_handler';
import { getExceptionsPreMultiListFindHandler } from './handlers/exceptions_pre_multi_list_find_handler';
import { getExceptionsPreSingleListFindHandler } from './handlers/exceptions_pre_single_list_find_handler';
import { getExceptionsPreImportHandler } from './handlers/exceptions_pre_import_handler';
export const registerListsPluginEndpointExtensionPoints = (
registerListsExtensionPoint: ListsServerExtensionRegistrar,
@ -67,4 +68,10 @@ export const registerListsPluginEndpointExtensionPoints = (
type: 'exceptionsListPreSingleListFind',
callback: getExceptionsPreSingleListFindHandler(endpointAppContextService),
});
// PRE-IMPORT
registerListsExtensionPoint({
type: 'exceptionsListPreImport',
callback: getExceptionsPreImportHandler(),
});
};

View file

@ -0,0 +1,46 @@
/*
* 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_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolution-list-constants';
import { BaseValidator } from './base_validator';
import { EndpointArtifactExceptionValidationError } from './errors';
export class HostIsolationExceptionsValidator extends BaseValidator {
static isHostIsolationException(listId: string): boolean {
return listId === ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID;
}
async validatePreGetOneItem(): Promise<void> {
await this.validateCanManageEndpointArtifacts();
}
async validatePreSummary(): Promise<void> {
await this.validateCanManageEndpointArtifacts();
}
async validatePreDeleteItem(): Promise<void> {
await this.validateCanManageEndpointArtifacts();
}
async validatePreExport(): Promise<void> {
await this.validateCanManageEndpointArtifacts();
}
async validatePreSingleListFind(): Promise<void> {
await this.validateCanManageEndpointArtifacts();
}
async validatePreMultiListFind(): Promise<void> {
await this.validateCanManageEndpointArtifacts();
}
async validatePreImport(): Promise<void> {
throw new EndpointArtifactExceptionValidationError(
'Import is not supported for Endpoint artifact exceptions'
);
}
}

View file

@ -348,6 +348,19 @@ export class Plugin implements ISecuritySolutionPlugin {
const savedObjectsClient = new SavedObjectsClient(core.savedObjects.createInternalRepository());
const registerIngestCallback = plugins.fleet?.registerExternalCallback;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const exceptionListClient = this.lists!.getExceptionListClient(
savedObjectsClient,
'kibana',
// execution of Lists plugin server extension points callbacks should be turned off
// here because most of the uses of this client will be in contexts where some endpoint
// validations (specifically those around authz) can not be done (due ot the lack of a `KibanaRequest`
// from where authz can be derived)
false
);
const { authz, agentService, packageService, packagePolicyService, agentPolicyService } =
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
plugins.fleet!;
let manifestManager: ManifestManager | undefined;
this.licensing$ = plugins.licensing.license$;
@ -355,7 +368,6 @@ export class Plugin implements ISecuritySolutionPlugin {
if (this.lists && plugins.taskManager && plugins.fleet) {
// Exceptions, Artifacts and Manifests start
const taskManager = plugins.taskManager;
const exceptionListClient = this.lists.getExceptionListClient(savedObjectsClient, 'kibana');
const artifactClient = new EndpointArtifactClient(
plugins.fleet.createArtifactsClient('endpoint')
);
@ -396,13 +408,6 @@ export class Plugin implements ISecuritySolutionPlugin {
this.policyWatcher.start(licenseService);
}
// 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({
fleetAuthzService: authz,
agentService,

View file

@ -17,6 +17,7 @@ import { ExceptionsListItemGenerator } from '../../../plugins/security_solution/
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';
import { EVENT_FILTER_LIST } from '../../../plugins/security_solution/public/management/pages/event_filters/constants';
import { HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION } from '../../../plugins/security_solution/public/management/pages/host_isolation_exceptions/constants';
export interface ArtifactTestData {
artifact: ExceptionListItemSchema;
@ -59,9 +60,9 @@ export class EndpointArtifactsTestResources extends FtrService {
.then(this.getHttpResponseFailureHandler())
.then((response) => response.body as ExceptionListItemSchema);
const { item_id: itemId, namespace_type: namespaceType } = artifact;
const { item_id: itemId, namespace_type: namespaceType, list_id: listId } = artifact;
this.log.info(`Created exception list item: ${itemId}`);
this.log.info(`Created exception list item [${listId}]: ${itemId}`);
const cleanup = async () => {
const deleteResponse = await this.supertest
@ -70,7 +71,9 @@ export class EndpointArtifactsTestResources extends FtrService {
.send()
.then(this.getHttpResponseFailureHandler([404]));
this.log.info(`Deleted exception list item: ${itemId} (${deleteResponse.status})`);
this.log.info(
`Deleted exception list item [${listId}]: ${itemId} (${deleteResponse.status})`
);
};
return {
@ -96,4 +99,13 @@ export class EndpointArtifactsTestResources extends FtrService {
return this.createExceptionItem(eventFilter);
}
async createHostIsolationException(
overrides: Partial<CreateExceptionListItemSchema> = {}
): Promise<ArtifactTestData> {
await this.ensureListExists(HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION);
const artifact = this.exceptionsGenerator.generateHostIsolationExceptionForCreate(overrides);
return this.createExceptionItem(artifact);
}
}

View file

@ -102,6 +102,9 @@ export function EndpointPolicyTestResourcesProvider({ getService }: FtrProviderC
`Endpoint package was not in response from ${SECURITY_PACKAGES_ROUTE}`
);
}
log.info(`Endpoint package version: ${endpointPackageInfo.version}`);
return Promise.resolve(endpointPackageInfo);
});
});
@ -217,6 +220,11 @@ export function EndpointPolicyTestResourcesProvider({ getService }: FtrProviderC
return logSupertestApiErrorAndThrow(`Unable to create Package Policy via Ingest!`, error);
}
log.info(
`Created Fleet Agent Policy: ${agentPolicy.id}\n`,
`Created Fleet Endpoint Package Policy: ${packagePolicy.id}`
);
return {
agentPolicy,
packagePolicy,

View file

@ -64,12 +64,12 @@ export default function ({ getService }: FtrProviderContext) {
const exceptionsGenerator = new ExceptionsListItemGenerator();
let trustedAppData: ArtifactTestData;
type TrustedAppApiCallsInterface = Array<{
type TrustedAppApiCallsInterface<BodyReturnType = unknown> = 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'>;
getBody: () => BodyReturnType;
}>;
beforeEach(async () => {
@ -84,7 +84,9 @@ export default function ({ getService }: FtrProviderContext) {
}
});
const trustedAppApiCalls: TrustedAppApiCallsInterface = [
const trustedAppApiCalls: TrustedAppApiCallsInterface<
Pick<ExceptionListItemSchema, 'os_types' | 'tags' | 'entries'>
> = [
{
method: 'post',
path: EXCEPTION_LIST_ITEM_URL,

View file

@ -0,0 +1,155 @@
/*
* 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, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
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 {
createUserAndRole,
deleteUserAndRole,
ROLES,
} from '../../common/services/security_solution';
import {
getImportExceptionsListSchemaMock,
toNdJsonString,
} from '../../../plugins/lists/common/schemas/request/import_exceptions_schema.mock';
export default function ({ getService }: FtrProviderContext) {
const USER = ROLES.detections_admin;
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const endpointPolicyTestResources = getService('endpointPolicyTestResources');
const endpointArtifactTestResources = getService('endpointArtifactTestResources');
type ApiCallsInterface<BodyReturnType = unknown> = Array<{
method: keyof Pick<typeof supertest, 'post' | 'put' | 'get' | 'delete' | 'patch'>;
info?: string;
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: () => BodyReturnType;
}>;
describe('Endpoint Host Isolation Exceptions artifacts (via lists plugin)', () => {
let fleetEndpointPolicy: PolicyTestResourceInfo;
let existingExceptionData: ArtifactTestData;
before(async () => {
// Create an endpoint policy in fleet we can work with
fleetEndpointPolicy = await endpointPolicyTestResources.createPolicy();
// create role/user
await createUserAndRole(getService, USER);
});
after(async () => {
if (fleetEndpointPolicy) {
await fleetEndpointPolicy.cleanup();
}
// delete role/user
await deleteUserAndRole(getService, USER);
});
beforeEach(async () => {
existingExceptionData = await endpointArtifactTestResources.createHostIsolationException({
tags: [`${BY_POLICY_ARTIFACT_TAG_PREFIX}${fleetEndpointPolicy.packagePolicy.id}`],
});
});
afterEach(async () => {
if (existingExceptionData) {
await existingExceptionData.cleanup();
}
});
it('should return 400 for import of endpoint exceptions', async () => {
await supertest
.post(`${EXCEPTION_LIST_URL}/_import?overwrite=false`)
.set('kbn-xsrf', 'true')
.attach(
'file',
Buffer.from(
toNdJsonString([
getImportExceptionsListSchemaMock(existingExceptionData.artifact.list_id),
])
),
'exceptions.ndjson'
)
.expect(400, {
status_code: 400,
message:
'EndpointArtifactError: Import is not supported for Endpoint artifact exceptions',
});
});
describe(`and user (${USER}) DOES NOT have authorization to manage endpoint security`, () => {
// Define a new array that includes the prior set from above, plus additional API calls that
// only have Authz validations setup
const allApiCalls: ApiCallsInterface = [
{
method: 'get',
info: 'single item',
get path() {
return `${EXCEPTION_LIST_ITEM_URL}?item_id=${existingExceptionData.artifact.item_id}&namespace_type=${existingExceptionData.artifact.namespace_type}`;
},
getBody: () => undefined,
},
{
method: 'get',
info: 'list summary',
get path() {
return `${EXCEPTION_LIST_URL}/summary?list_id=${existingExceptionData.artifact.list_id}&namespace_type=${existingExceptionData.artifact.namespace_type}`;
},
getBody: () => undefined,
},
{
method: 'delete',
info: 'single item',
get path() {
return `${EXCEPTION_LIST_ITEM_URL}?item_id=${existingExceptionData.artifact.item_id}&namespace_type=${existingExceptionData.artifact.namespace_type}`;
},
getBody: () => undefined,
},
{
method: 'post',
info: 'list export',
get path() {
return `${EXCEPTION_LIST_URL}/_export?list_id=${existingExceptionData.artifact.list_id}&namespace_type=${existingExceptionData.artifact.namespace_type}&id=1`;
},
getBody: () => undefined,
},
{
method: 'get',
info: 'find items',
get path() {
return `${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${existingExceptionData.artifact.list_id}&namespace_type=${existingExceptionData.artifact.namespace_type}&page=1&per_page=1&sort_field=name&sort_order=asc`;
},
getBody: () => undefined,
},
];
for (const apiCall of allApiCalls) {
it(`should error on [${apiCall.method}]${
apiCall.info ? ` ${apiCall.info}` : ''
}`, async () => {
await supertestWithoutAuth[apiCall.method](apiCall.path)
.auth(ROLES.detections_admin, 'changeme')
.set('kbn-xsrf', 'true')
.send(apiCall.getBody())
.expect(403, {
status_code: 403,
message: 'EndpointArtifactError: Endpoint authorization failure',
});
});
}
});
});
}

View file

@ -34,5 +34,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider
loadTestFile(require.resolve('./endpoint_authz'));
loadTestFile(require.resolve('./endpoint_artifacts'));
loadTestFile(require.resolve('./endpoint_artifacts/event_filter'));
loadTestFile(require.resolve('./endpoint_host_isolation_exceptions_artifacts'));
});
}