mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
940c4e2833
commit
997988fac2
22 changed files with 524 additions and 203 deletions
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue