[Security Solution][Endpoint] Add Event Filters api validations get, find, delete, export, summary and import (#124071)

* Add additional validator methods to the `EventFilterValidator`
* Event Filters validations for Delete, export, get one, multi/single list find and summary apis
* FTR tests for Event filters get, delete, import, export, summary and find
This commit is contained in:
Paul Tavares 2022-01-31 18:56:57 -05:00 committed by GitHub
parent ee081010d8
commit ddb3f4f461
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 194 additions and 23 deletions

View file

@ -17,6 +17,10 @@ export const getExceptionsPreCreateItemHandler = (
endpointAppContext: EndpointAppContextService
): ValidatorCallback => {
return async function ({ data, context: { request } }): Promise<CreateExceptionListItemOptions> {
if (data.namespaceType !== 'agnostic') {
return data;
}
// Validate trusted apps
if (TrustedAppValidator.isTrustedApp(data)) {
return new TrustedAppValidator(endpointAppContext, request).validatePreCreateItem(data);

View file

@ -10,6 +10,7 @@ import { EndpointAppContextService } from '../../../endpoint/endpoint_app_contex
import { ExceptionsListPreDeleteItemServerExtension } from '../../../../../lists/server';
import { TrustedAppValidator } from '../validators/trusted_app_validator';
import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
import { EventFilterValidator } from '../validators';
type ValidatorCallback = ExceptionsListPreDeleteItemServerExtension['callback'];
export const getExceptionsPreDeleteItemHandler = (
@ -31,13 +32,16 @@ export const getExceptionsPreDeleteItemHandler = (
return data;
}
const { list_id: listId } = exceptionItem;
// Validate Trusted Applications
if (TrustedAppValidator.isTrustedApp({ listId: exceptionItem.list_id })) {
if (TrustedAppValidator.isTrustedApp({ listId })) {
await new TrustedAppValidator(endpointAppContextService, request).validatePreDeleteItem();
return data;
}
// Host Isolation Exception
if (HostIsolationExceptionsValidator.isHostIsolationException(exceptionItem.list_id)) {
if (HostIsolationExceptionsValidator.isHostIsolationException(listId)) {
await new HostIsolationExceptionsValidator(
endpointAppContextService,
request
@ -45,6 +49,12 @@ export const getExceptionsPreDeleteItemHandler = (
return data;
}
// Event Filter validation
if (EventFilterValidator.isEventFilter({ listId })) {
await new EventFilterValidator(endpointAppContextService, request).validatePreDeleteItem();
return data;
}
return data;
};
};

View file

@ -9,12 +9,17 @@ import { EndpointAppContextService } from '../../../endpoint/endpoint_app_contex
import { ExceptionsListPreExportServerExtension } from '../../../../../lists/server';
import { TrustedAppValidator } from '../validators/trusted_app_validator';
import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
import { EventFilterValidator } from '../validators';
type ValidatorCallback = ExceptionsListPreExportServerExtension['callback'];
export const getExceptionsPreExportHandler = (
endpointAppContextService: EndpointAppContextService
): ValidatorCallback => {
return async function ({ data, context: { request, exceptionListClient } }) {
if (data.namespaceType !== 'agnostic') {
return data;
}
const { listId: maybeListId, id } = data;
let listId: string | null | undefined = maybeListId;
@ -40,6 +45,12 @@ export const getExceptionsPreExportHandler = (
return data;
}
// Event Filter validations
if (EventFilterValidator.isEventFilter({ listId })) {
await new EventFilterValidator(endpointAppContextService, request).validatePreExport();
return data;
}
return data;
};
};

View file

@ -10,6 +10,7 @@ import { EndpointAppContextService } from '../../../endpoint/endpoint_app_contex
import { ExceptionsListPreGetOneItemServerExtension } from '../../../../../lists/server';
import { TrustedAppValidator } from '../validators/trusted_app_validator';
import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
import { EventFilterValidator } from '../validators';
type ValidatorCallback = ExceptionsListPreGetOneItemServerExtension['callback'];
export const getExceptionsPreGetOneHandler = (
@ -31,13 +32,16 @@ export const getExceptionsPreGetOneHandler = (
return data;
}
const listId = exceptionItem.list_id;
// Validate Trusted Applications
if (TrustedAppValidator.isTrustedApp({ listId: exceptionItem.list_id })) {
if (TrustedAppValidator.isTrustedApp({ listId })) {
await new TrustedAppValidator(endpointAppContextService, request).validatePreGetOneItem();
return data;
}
// validate Host Isolation Exception
if (HostIsolationExceptionsValidator.isHostIsolationException(exceptionItem.list_id)) {
if (HostIsolationExceptionsValidator.isHostIsolationException(listId)) {
await new HostIsolationExceptionsValidator(
endpointAppContextService,
request
@ -45,6 +49,12 @@ export const getExceptionsPreGetOneHandler = (
return data;
}
// Event Filters Exception
if (EventFilterValidator.isEventFilter({ listId })) {
await new EventFilterValidator(endpointAppContextService, request).validatePreGetOneItem();
return data;
}
return data;
};
};

View file

@ -9,6 +9,7 @@ import { EndpointAppContextService } from '../../../endpoint/endpoint_app_contex
import { ExceptionsListPreMultiListFindServerExtension } from '../../../../../lists/server';
import { TrustedAppValidator } from '../validators/trusted_app_validator';
import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
import { EventFilterValidator } from '../validators';
type ValidatorCallback = ExceptionsListPreMultiListFindServerExtension['callback'];
export const getExceptionsPreMultiListFindHandler = (
@ -33,6 +34,12 @@ export const getExceptionsPreMultiListFindHandler = (
return data;
}
// Event Filters Exceptions
if (data.listId.some((listId) => EventFilterValidator.isEventFilter({ listId }))) {
await new EventFilterValidator(endpointAppContextService, request).validatePreMultiListFind();
return data;
}
return data;
};
};

View file

@ -9,6 +9,7 @@ import { EndpointAppContextService } from '../../../endpoint/endpoint_app_contex
import { ExceptionsListPreSingleListFindServerExtension } from '../../../../../lists/server';
import { TrustedAppValidator } from '../validators/trusted_app_validator';
import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
import { EventFilterValidator } from '../validators';
type ValidatorCallback = ExceptionsListPreSingleListFindServerExtension['callback'];
export const getExceptionsPreSingleListFindHandler = (
@ -19,12 +20,16 @@ export const getExceptionsPreSingleListFindHandler = (
return data;
}
const { listId } = data;
// Validate Host Isolation Exceptions
if (TrustedAppValidator.isTrustedApp({ listId: data.listId })) {
if (TrustedAppValidator.isTrustedApp({ listId })) {
await new TrustedAppValidator(endpointAppContextService, request).validatePreSingleListFind();
return data;
}
if (HostIsolationExceptionsValidator.isHostIsolationException(data.listId)) {
// Host Isolation Exceptions
if (HostIsolationExceptionsValidator.isHostIsolationException(listId)) {
await new HostIsolationExceptionsValidator(
endpointAppContextService,
request
@ -32,6 +37,15 @@ export const getExceptionsPreSingleListFindHandler = (
return data;
}
// Event Filters Exceptions
if (EventFilterValidator.isEventFilter({ listId })) {
await new EventFilterValidator(
endpointAppContextService,
request
).validatePreSingleListFind();
return data;
}
return data;
};
};

View file

@ -7,7 +7,7 @@
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import { ExceptionsListPreSummaryServerExtension } from '../../../../../lists/server';
import { TrustedAppValidator } from '../validators';
import { TrustedAppValidator, EventFilterValidator } from '../validators';
import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
type ValidatorCallback = ExceptionsListPreSummaryServerExtension['callback'];
@ -15,6 +15,10 @@ export const getExceptionsPreSummaryHandler = (
endpointAppContextService: EndpointAppContextService
): ValidatorCallback => {
return async function ({ data, context: { request, exceptionListClient } }) {
if (data.namespaceType !== 'agnostic') {
return data;
}
const { listId: maybeListId, id } = data;
let listId: string | null | undefined = maybeListId;
@ -40,6 +44,12 @@ export const getExceptionsPreSummaryHandler = (
return data;
}
// Event Filter Exceptions
if (EventFilterValidator.isEventFilter({ listId })) {
await new EventFilterValidator(endpointAppContextService, request).validatePreSummary();
return data;
}
return data;
};
};

View file

@ -101,4 +101,34 @@ export class EventFilterValidator extends BaseValidator {
throw new EndpointArtifactExceptionValidationError(error.message);
}
}
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

@ -5,7 +5,7 @@
* 2.0.
*/
import { EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants';
import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
@ -18,6 +18,10 @@ import {
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 supertest = getService('supertest');
@ -26,6 +30,7 @@ export default function ({ getService }: FtrProviderContext) {
const endpointArtifactTestResources = getService('endpointArtifactTestResources');
describe('Endpoint artifacts (via lists plugin): Event Filters', () => {
const USER = ROLES.detections_admin;
let fleetEndpointPolicy: PolicyTestResourceInfo;
before(async () => {
@ -33,7 +38,7 @@ export default function ({ getService }: FtrProviderContext) {
fleetEndpointPolicy = await endpointPolicyTestResources.createPolicy();
// create role/user
await createUserAndRole(getService, ROLES.detections_admin);
await createUserAndRole(getService, USER);
});
after(async () => {
@ -42,7 +47,7 @@ export default function ({ getService }: FtrProviderContext) {
}
// delete role/user
await deleteUserAndRole(getService, ROLES.detections_admin);
await deleteUserAndRole(getService, USER);
});
const anEndpointArtifactError = (res: { body: { message: string } }) => {
@ -63,21 +68,25 @@ export default function ({ getService }: FtrProviderContext) {
const exceptionsGenerator = new ExceptionsListItemGenerator();
let eventFilterData: ArtifactTestData;
type EventFilterApiCallsInterface = Array<{
method: keyof Pick<typeof supertest, 'post' | 'put'>;
type UnknownBodyGetter = () => unknown;
type PutPostBodyGetter = (
overrides?: Partial<ExceptionListItemSchema>
) => Pick<ExceptionListItemSchema, 'os_types' | 'tags' | 'entries'>;
type EventFilterApiCallsInterface<BodyGetter = UnknownBodyGetter> = 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: (
overrides: Partial<ExceptionListItemSchema>
) => Pick<ExceptionListItemSchema, 'os_types' | 'tags' | 'entries'>;
getBody: BodyGetter;
}>;
const eventFilterCalls: EventFilterApiCallsInterface = [
const eventFilterCalls: EventFilterApiCallsInterface<PutPostBodyGetter> = [
{
method: 'post',
path: EXCEPTION_LIST_ITEM_URL,
getBody: (overrides) =>
getBody: (overrides = {}) =>
exceptionsGenerator.generateEventFilterForCreate({
tags: eventFilterData.artifact.tags,
...overrides,
@ -86,7 +95,7 @@ export default function ({ getService }: FtrProviderContext) {
{
method: 'put',
path: EXCEPTION_LIST_ITEM_URL,
getBody: (overrides) =>
getBody: (overrides = {}) =>
exceptionsGenerator.generateEventFilterForUpdate({
id: eventFilterData.artifact.id,
item_id: eventFilterData.artifact.item_id,
@ -109,6 +118,24 @@ export default function ({ getService }: FtrProviderContext) {
}
});
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(eventFilterData.artifact.list_id)])
),
'exceptions.ndjson'
)
.expect(400, {
status_code: 400,
message:
'EndpointArtifactError: Import is not supported for Endpoint artifact exceptions',
});
});
describe('and has authorization to manage endpoint security', () => {
for (const eventFilterCall of eventFilterCalls) {
it(`should error on [${eventFilterCall.method} if invalid field`, async () => {
@ -159,13 +186,61 @@ export default function ({ getService }: FtrProviderContext) {
}
});
describe('and user DOES NOT have authorization to manage endpoint security', () => {
for (const eventFilterCall of eventFilterCalls) {
it(`should 403 on [${eventFilterCall.method}]`, async () => {
await supertestWithoutAuth[eventFilterCall.method](eventFilterCall.path)
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: EventFilterApiCallsInterface<PutPostBodyGetter | UnknownBodyGetter> = [
...eventFilterCalls,
{
method: 'get',
info: 'single item',
get path() {
return `${EXCEPTION_LIST_ITEM_URL}?item_id=${eventFilterData.artifact.item_id}&namespace_type=${eventFilterData.artifact.namespace_type}`;
},
getBody: () => undefined,
},
{
method: 'get',
info: 'list summary',
get path() {
return `${EXCEPTION_LIST_URL}/summary?list_id=${eventFilterData.artifact.list_id}&namespace_type=${eventFilterData.artifact.namespace_type}`;
},
getBody: () => undefined,
},
{
method: 'delete',
info: 'single item',
get path() {
return `${EXCEPTION_LIST_ITEM_URL}?item_id=${eventFilterData.artifact.item_id}&namespace_type=${eventFilterData.artifact.namespace_type}`;
},
getBody: () => undefined,
},
{
method: 'post',
info: 'list export',
get path() {
return `${EXCEPTION_LIST_URL}/_export?list_id=${eventFilterData.artifact.list_id}&namespace_type=${eventFilterData.artifact.namespace_type}&id=1`;
},
getBody: () => undefined,
},
{
method: 'get',
info: 'find items',
get path() {
return `${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${eventFilterData.artifact.list_id}&namespace_type=${eventFilterData.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(eventFilterCall.getBody({}))
.send(apiCall.getBody())
.expect(403, {
status_code: 403,
message: 'EndpointArtifactError: Endpoint authorization failure',