Skip to content

Commit

Permalink
Merge branch 'master' into handle-disabled-process-collection-v2
Browse files Browse the repository at this point in the history
  • Loading branch information
elasticmachine authored Jul 29, 2020
2 parents d33b537 + e105bc5 commit 62d55fb
Show file tree
Hide file tree
Showing 34 changed files with 1,528 additions and 314 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/lists/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ export const ENDPOINT_LIST_NAME = 'Elastic Endpoint Security Exception List';

/** The description of the single global space agnostic endpoint list */
export const ENDPOINT_LIST_DESCRIPTION = 'Elastic Endpoint Security Exception List';

export const MAX_EXCEPTION_LIST_SIZE = 10000;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { IRouter } from 'kibana/server';

import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants';
import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants';
import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps';
import { validate } from '../../common/siem_common_deps';
import {
Expand All @@ -16,6 +16,7 @@ import {
} from '../../common/schemas';

import { getExceptionListClient } from './utils/get_exception_list_client';
import { validateExceptionListSize } from './validate';

export const createEndpointListItemRoute = (router: IRouter): void => {
router.post(
Expand Down Expand Up @@ -71,6 +72,18 @@ export const createEndpointListItemRoute = (router: IRouter): void => {
if (errors != null) {
return siemResponse.error({ body: errors, statusCode: 500 });
} else {
const listSizeError = await validateExceptionListSize(
exceptionLists,
ENDPOINT_LIST_ID,
'agnostic'
);
if (listSizeError != null) {
await exceptionLists.deleteExceptionListItemById({
id: createdList.id,
namespaceType: 'agnostic',
});
return siemResponse.error(listSizeError);
}
return response.ok({ body: validated ?? {} });
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {

import { getExceptionListClient } from './utils/get_exception_list_client';
import { endpointDisallowedFields } from './endpoint_disallowed_fields';
import { validateExceptionListSize } from './validate';

export const createExceptionListItemRoute = (router: IRouter): void => {
router.post(
Expand Down Expand Up @@ -104,6 +105,18 @@ export const createExceptionListItemRoute = (router: IRouter): void => {
if (errors != null) {
return siemResponse.error({ body: errors, statusCode: 500 });
} else {
const listSizeError = await validateExceptionListSize(
exceptionLists,
listId,
namespaceType
);
if (listSizeError != null) {
await exceptionLists.deleteExceptionListItemById({
id: createdList.id,
namespaceType,
});
return siemResponse.error(listSizeError);
}
return response.ok({ body: validated ?? {} });
}
}
Expand Down
56 changes: 56 additions & 0 deletions x-pack/plugins/lists/server/routes/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { ExceptionListClient } from '../services/exception_lists/exception_list_client';
import { MAX_EXCEPTION_LIST_SIZE } from '../../common/constants';
import { foundExceptionListItemSchema } from '../../common/schemas';
import { NamespaceType } from '../../common/schemas/types';
import { validate } from '../../common/siem_common_deps';

export const validateExceptionListSize = async (
exceptionLists: ExceptionListClient,
listId: string,
namespaceType: NamespaceType
): Promise<{ body: string; statusCode: number } | null> => {
const exceptionListItems = await exceptionLists.findExceptionListItem({
filter: undefined,
listId,
namespaceType,
page: undefined,
perPage: undefined,
sortField: undefined,
sortOrder: undefined,
});
if (exceptionListItems == null) {
// If exceptionListItems is null then we couldn't find the list so it may have been deleted
return {
body: `Unable to find list id: ${listId} to verify max exception list size`,
statusCode: 500,
};
}
const [validatedItems, err] = validate(exceptionListItems, foundExceptionListItemSchema);
if (err != null) {
return {
body: err,
statusCode: 500,
};
}
// Unnecessary since validatedItems comes from exceptionListItems which is already
// checked for null, but typescript fails to detect that
if (validatedItems == null) {
return {
body: `Unable to find list id: ${listId} to verify max exception list size`,
statusCode: 500,
};
}
if (validatedItems.total > MAX_EXCEPTION_LIST_SIZE) {
return {
body: `Failed to add exception item, exception list would exceed max size of ${MAX_EXCEPTION_LIST_SIZE}`,
statusCode: 400,
};
}
return null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SavedObjectsClientContract } from 'kibana/server';

import {
ExceptionListItemSchema,
Id,
IdOrUndefined,
ItemIdOrUndefined,
NamespaceType,
Expand All @@ -23,6 +24,12 @@ interface DeleteExceptionListItemOptions {
savedObjectsClient: SavedObjectsClientContract;
}

interface DeleteExceptionListItemByIdOptions {
id: Id;
namespaceType: NamespaceType;
savedObjectsClient: SavedObjectsClientContract;
}

export const deleteExceptionListItem = async ({
itemId,
id,
Expand All @@ -43,3 +50,12 @@ export const deleteExceptionListItem = async ({
return exceptionListItem;
}
};

export const deleteExceptionListItemById = async ({
id,
namespaceType,
savedObjectsClient,
}: DeleteExceptionListItemByIdOptions): Promise<void> => {
const savedObjectType = getSavedObjectType({ namespaceType });
await savedObjectsClient.delete(savedObjectType, id);
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
CreateExceptionListItemOptions,
CreateExceptionListOptions,
DeleteEndpointListItemOptions,
DeleteExceptionListItemByIdOptions,
DeleteExceptionListItemOptions,
DeleteExceptionListOptions,
FindEndpointListItemOptions,
Expand All @@ -40,7 +41,7 @@ import { createExceptionListItem } from './create_exception_list_item';
import { updateExceptionList } from './update_exception_list';
import { updateExceptionListItem } from './update_exception_list_item';
import { deleteExceptionList } from './delete_exception_list';
import { deleteExceptionListItem } from './delete_exception_list_item';
import { deleteExceptionListItem, deleteExceptionListItemById } from './delete_exception_list_item';
import { findExceptionListItem } from './find_exception_list_item';
import { findExceptionList } from './find_exception_list';
import { findExceptionListsItem } from './find_exception_list_items';
Expand Down Expand Up @@ -326,6 +327,18 @@ export class ExceptionListClient {
});
};

public deleteExceptionListItemById = async ({
id,
namespaceType,
}: DeleteExceptionListItemByIdOptions): Promise<void> => {
const { savedObjectsClient } = this;
return deleteExceptionListItemById({
id,
namespaceType,
savedObjectsClient,
});
};

/**
* This is the same as "deleteExceptionListItem" except it applies specifically to the endpoint list.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
ExceptionListType,
ExceptionListTypeOrUndefined,
FilterOrUndefined,
Id,
IdOrUndefined,
Immutable,
ItemId,
Expand Down Expand Up @@ -93,6 +94,11 @@ export interface DeleteExceptionListItemOptions {
namespaceType: NamespaceType;
}

export interface DeleteExceptionListItemByIdOptions {
id: Id;
namespaceType: NamespaceType;
}

export interface DeleteEndpointListItemOptions {
id: IdOrUndefined;
itemId: ItemIdOrUndefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { TABLE_COLUMN_EVENTS_MESSAGE } from '../screens/hosts/external_events';
import { waitsForEventsToBeLoaded, openEventsViewerFieldsBrowser } from '../tasks/hosts/events';
import { removeColumn, resetFields } from '../tasks/timeline';

describe('persistent timeline', () => {
// FLAKY: https://github.com/elastic/kibana/issues/72339
describe.skip('persistent timeline', () => {
before(() => {
loginAndWaitForPage(HOSTS_URL);
openEvents();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { KibanaReactContextValue } from '../../../../../../src/plugins/kibana_react/public';
import { StartServices } from '../../types';
import { DataAccessLayer } from '../types';
import {
ResolverRelatedEvents,
ResolverTree,
ResolverEntityIndex,
} from '../../../common/endpoint/types';
import { DEFAULT_INDEX_KEY as defaultIndexKey } from '../../../common/constants';

/**
* The data access layer for resolver. All communication with the Kibana server is done through this object. This object is provided to Resolver. In tests, a mock data access layer can be used instead.
*/
export function dataAccessLayerFactory(
context: KibanaReactContextValue<StartServices>
): DataAccessLayer {
const dataAccessLayer: DataAccessLayer = {
/**
* Used to get non-process related events for a node.
*/
async relatedEvents(entityID: string): Promise<ResolverRelatedEvents> {
return context.services.http.get(`/api/endpoint/resolver/${entityID}/events`, {
query: { events: 100 },
});
},
/**
* Used to get descendant and ancestor process events for a node.
*/
async resolverTree(entityID: string, signal: AbortSignal): Promise<ResolverTree> {
return context.services.http.get(`/api/endpoint/resolver/${entityID}`, {
signal,
});
},

/**
* Used to get the default index pattern from the SIEM application.
*/
indexPatterns(): string[] {
return context.services.uiSettings.get(defaultIndexKey);
},

/**
* Used to get the entity_id for an _id.
*/
async entities({
_id,
indices,
signal,
}: {
_id: string;
indices: string[];
signal: AbortSignal;
}): Promise<ResolverEntityIndex> {
return context.services.http.get('/api/endpoint/resolver/entity', {
signal,
query: {
_id,
indices,
},
});
},
};
return dataAccessLayer;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import {
ResolverRelatedEvents,
ResolverTree,
ResolverEntityIndex,
} from '../../../../common/endpoint/types';
import { mockEndpointEvent } from '../../store/mocks/endpoint_event';
import { mockTreeWithNoAncestorsAnd2Children } from '../../store/mocks/resolver_tree';
import { DataAccessLayer } from '../../types';

interface Metadata {
/**
* The `_id` of the document being analyzed.
*/
databaseDocumentID: string;
/**
* A record of entityIDs to be used in tests assertions.
*/
entityIDs: {
/**
* The entityID of the node related to the document being analyzed.
*/
origin: 'origin';
/**
* The entityID of the first child of the origin.
*/
firstChild: 'firstChild';
/**
* The entityID of the second child of the origin.
*/
secondChild: 'secondChild';
};
}

/**
* A simple mock dataAccessLayer possible that returns a tree with 0 ancestors and 2 direct children. 1 related event is returned. The parameter to `entities` is ignored.
*/
export function oneAncestorTwoChildren(): { dataAccessLayer: DataAccessLayer; metadata: Metadata } {
const metadata: Metadata = {
databaseDocumentID: '_id',
entityIDs: { origin: 'origin', firstChild: 'firstChild', secondChild: 'secondChild' },
};
return {
metadata,
dataAccessLayer: {
/**
* Fetch related events for an entity ID
*/
relatedEvents(entityID: string): Promise<ResolverRelatedEvents> {
return Promise.resolve({
entityID,
events: [
mockEndpointEvent({
entityID,
name: 'event',
timestamp: 0,
}),
],
nextEvent: null,
});
},

/**
* Fetch a ResolverTree for a entityID
*/
resolverTree(): Promise<ResolverTree> {
return Promise.resolve(
mockTreeWithNoAncestorsAnd2Children({
originID: metadata.entityIDs.origin,
firstChildID: metadata.entityIDs.firstChild,
secondChildID: metadata.entityIDs.secondChild,
})
);
},

/**
* Get an array of index patterns that contain events.
*/
indexPatterns(): string[] {
return ['index pattern'];
},

/**
* Get entities matching a document.
*/
entities(): Promise<ResolverEntityIndex> {
return Promise.resolve([{ entity_id: metadata.entityIDs.origin }]);
},
},
};
}
Loading

0 comments on commit 62d55fb

Please sign in to comment.