[Security Solution] Exceptions TTL Follow-up (#151952)

This commit is contained in:
Davis Plumlee 2023-03-03 18:56:39 -05:00 committed by GitHub
parent d7bd1d210b
commit 203fa3a955
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 249 additions and 64 deletions

View file

@ -22,7 +22,6 @@ import { description } from '../../common/description';
import { name } from '../../common/name';
import { meta } from '../../common/meta';
import { tags } from '../../common/tags';
import { ExpireTimeOrUndefined, expireTimeOrUndefined } from '../../common';
export const createEndpointListItemSchema = t.intersection([
t.exact(
@ -40,7 +39,6 @@ export const createEndpointListItemSchema = t.intersection([
meta, // defaults to undefined if not set during decode
os_types: osTypeArrayOrUndefined, // defaults to empty array if not set during decode
tags, // defaults to empty array if not set during decode
expire_time: expireTimeOrUndefined, // defaults to undefined if not set during decode
})
),
]);
@ -50,12 +48,11 @@ export type CreateEndpointListItemSchema = t.OutputOf<typeof createEndpointListI
// This type is used after a decode since some things are defaults after a decode.
export type CreateEndpointListItemSchemaDecoded = Omit<
RequiredKeepUndefined<t.TypeOf<typeof createEndpointListItemSchema>>,
'tags' | 'item_id' | 'entries' | 'comments' | 'os_types' | 'expire_time'
'tags' | 'item_id' | 'entries' | 'comments' | 'os_types'
> & {
comments: CreateCommentsArray;
tags: Tags;
item_id: ItemId;
entries: EntriesArray;
os_types: OsTypeArray;
expire_time: ExpireTimeOrUndefined;
};

View file

@ -21,7 +21,6 @@ import { Tags, tags } from '../../common/tags';
import { RequiredKeepUndefined } from '../../common/required_keep_undefined';
import { UpdateCommentsArray } from '../../common/update_comment';
import { EntriesArray } from '../../common/entries';
import { ExpireTimeOrUndefined, expireTimeOrUndefined } from '../../common';
export const updateEndpointListItemSchema = t.intersection([
t.exact(
@ -41,7 +40,6 @@ export const updateEndpointListItemSchema = t.intersection([
meta, // defaults to undefined if not set during decode
os_types: osTypeArrayOrUndefined, // defaults to empty array if not set during decode
tags, // defaults to empty array if not set during decode
expire_time: expireTimeOrUndefined,
})
),
]);
@ -51,11 +49,10 @@ export type UpdateEndpointListItemSchema = t.OutputOf<typeof updateEndpointListI
// This type is used after a decode since some things are defaults after a decode.
export type UpdateEndpointListItemSchemaDecoded = Omit<
RequiredKeepUndefined<t.TypeOf<typeof updateEndpointListItemSchema>>,
'tags' | 'entries' | 'comments' | 'expire_time'
'tags' | 'entries' | 'comments'
> & {
comments: UpdateCommentsArray;
tags: Tags;
entries: EntriesArray;
os_types: OsTypeArray;
expire_time: ExpireTimeOrUndefined;
};

View file

@ -43,7 +43,6 @@ export const createEndpointListItemRoute = (router: ListsPluginRouter): void =>
comments,
description,
entries,
expire_time: expireTime,
item_id: itemId,
os_types: osTypes,
type,
@ -63,7 +62,6 @@ export const createEndpointListItemRoute = (router: ListsPluginRouter): void =>
comments,
description,
entries,
expireTime,
itemId,
meta,
name,

View file

@ -49,7 +49,6 @@ export const updateEndpointListItemRoute = (router: ListsPluginRouter): void =>
entries,
item_id: itemId,
tags,
expire_time: expireTime,
} = request.body;
const exceptionLists = await getExceptionListClient(context);
const exceptionListItem = await exceptionLists.updateEndpointListItem({
@ -57,7 +56,6 @@ export const updateEndpointListItemRoute = (router: ListsPluginRouter): void =>
comments,
description,
entries,
expireTime,
id,
itemId,
meta,

View file

@ -65,7 +65,7 @@ export const updateExceptionListItemRoute = (router: ListsPluginRouter): void =>
});
} else {
const exceptionLists = await getExceptionListClient(context);
const exceptionListItem = await exceptionLists.updateExceptionListItem({
const exceptionListItem = await exceptionLists.updateOverwriteExceptionListItem({
_version,
comments,
description,

View file

@ -83,6 +83,7 @@ export const duplicateExceptionListAndItems = async ({
comments: [],
description: item.description,
entries: item.entries,
expire_time: item.expire_time,
item_id: newItemId,
list_id: newlyCreatedList.list_id,
meta: item.meta,

View file

@ -97,6 +97,7 @@ import { findExceptionListsItemPointInTimeFinder } from './find_exception_list_i
import { findValueListExceptionListItemsPointInTimeFinder } from './find_value_list_exception_list_items_point_in_time_finder';
import { findExceptionListItemPointInTimeFinder } from './find_exception_list_item_point_in_time_finder';
import { duplicateExceptionListAndItems } from './duplicate_exception_list';
import { updateOverwriteExceptionListItem } from './update_overwrite_exception_list_item';
/**
* Class for use for exceptions that are with trusted applications or
@ -287,7 +288,6 @@ export class ExceptionListClient {
comments,
description,
entries,
expireTime,
itemId,
meta,
name,
@ -301,7 +301,7 @@ export class ExceptionListClient {
comments,
description,
entries,
expireTime,
expireTime: undefined, // Not currently used with endpoint exceptions
itemId,
listId: ENDPOINT_LIST_ID,
meta,
@ -358,7 +358,6 @@ export class ExceptionListClient {
comments,
description,
entries,
expireTime,
id,
itemId,
meta,
@ -374,7 +373,7 @@ export class ExceptionListClient {
comments,
description,
entries,
expireTime,
expireTime: undefined, // Not currently used with endpoint exceptions
id,
itemId,
meta,
@ -579,6 +578,11 @@ export class ExceptionListClient {
/**
* Update an existing exception list item
*
* NOTE: This method will PATCH the targeted exception list item, not fully overwrite it.
* Any undefined fields passed in will not be changed in the existing record. To unset any
* fields use the `updateOverwriteExceptionListItem` method
*
* @param options
* @param options._version document version
* @param options.comments user comments attached to item
@ -647,6 +651,81 @@ export class ExceptionListClient {
});
};
/**
* Update an existing exception list item using the overwrite method in order to behave
* more like a PUT request rather than a PATCH request.
*
* This was done in order to correctly unset types via update which cannot be accomplished
* using the regular `updateExceptionItem` method. All other results of the methods are identical
*
* @param options
* @param options._version document version
* @param options.comments user comments attached to item
* @param options.entries item exception entries logic
* @param options.id the "id" of the exception list item
* @param options.description a description of the exception list
* @param options.itemId the "item_id" of the exception list item
* @param options.meta Optional meta data about the exception list item
* @param options.name the "name" of the exception list
* @param options.namespaceType saved object namespace (single | agnostic)
* @param options.osTypes item os types to apply
* @param options.tags user assigned tags of exception list
* @param options.type container type
* @returns the updated exception list item or null if none exists
*/
public updateOverwriteExceptionListItem = async ({
_version,
comments,
description,
entries,
expireTime,
id,
itemId,
meta,
name,
namespaceType,
osTypes,
tags,
type,
}: UpdateExceptionListItemOptions): Promise<ExceptionListItemSchema | null> => {
const { savedObjectsClient, user } = this;
let updatedItem: UpdateExceptionListItemOptions = {
_version,
comments,
description,
entries,
expireTime,
id,
itemId,
meta,
name,
namespaceType,
osTypes,
tags,
type,
};
if (this.enableServerExtensionPoints) {
updatedItem = await this.serverExtensionsClient.pipeRun(
'exceptionsListPreUpdateItem',
updatedItem,
this.getServerExtensionCallbackContext(),
(data) => {
return validateData(
updateExceptionListItemSchema,
transformUpdateExceptionListItemOptionsToUpdateExceptionListItemSchema(data)
);
}
);
}
return updateOverwriteExceptionListItem({
...updatedItem,
savedObjectsClient,
user,
});
};
/**
* Delete an exception list item by either id or item_id
* @param options

View file

@ -274,8 +274,6 @@ export interface CreateEndpointListItemOptions {
comments: CreateCommentsArray;
/** The entries of the endpoint list item */
entries: EntriesArray;
/** an optional datetime string with an expiration time */
expireTime: ExpireTimeOrUndefined;
/** The item id of the list item */
itemId: ItemId;
/** The name of the list item */
@ -347,8 +345,6 @@ export interface UpdateEndpointListItemOptions {
comments: UpdateCommentsArray;
/** The entries of the endpoint list item */
entries: EntriesArray;
/** an optional datetime string with an expiration time */
expireTime: ExpireTimeOrUndefined;
/** The id of the list item (Either this or itemId has to be defined) */
id: IdOrUndefined;
/** The item id of the list item (Either this or id has to be defined) */

View file

@ -32,7 +32,7 @@ import {
} from './utils';
import { getExceptionListItem } from './get_exception_list_item';
interface UpdateExceptionListItemOptions {
export interface UpdateExceptionListItemOptions {
id: IdOrUndefined;
comments: UpdateCommentsArrayOrUndefined;
_version: _VersionOrUndefined;

View file

@ -0,0 +1,85 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { getSavedObjectType } from '@kbn/securitysolution-list-utils';
import { ExceptionListSoSchema } from '../../schemas/saved_objects';
import {
transformSavedObjectUpdateToExceptionListItem,
transformUpdateCommentsToComments,
} from './utils';
import { getExceptionListItem } from './get_exception_list_item';
import { UpdateExceptionListItemOptions } from './update_exception_list_item';
export const updateOverwriteExceptionListItem = async ({
_version,
comments,
entries,
expireTime,
id,
savedObjectsClient,
namespaceType,
name,
osTypes,
description,
itemId,
meta,
user,
tags,
type,
}: UpdateExceptionListItemOptions): Promise<ExceptionListItemSchema | null> => {
const savedObjectType = getSavedObjectType({ namespaceType });
const exceptionListItem = await getExceptionListItem({
id,
itemId,
namespaceType,
savedObjectsClient,
});
if (exceptionListItem == null) {
return null;
} else {
const transformedComments = transformUpdateCommentsToComments({
comments,
existingComments: exceptionListItem.comments,
user,
});
const savedObject = await savedObjectsClient.create<ExceptionListSoSchema>(
savedObjectType,
{
comments: transformedComments,
created_at: exceptionListItem.created_at,
created_by: exceptionListItem.created_by,
description: description ?? exceptionListItem.description,
entries,
expire_time: expireTime,
immutable: undefined,
item_id: itemId,
list_id: exceptionListItem.list_id,
list_type: 'item',
meta,
name: name ?? exceptionListItem.name,
os_types: osTypes,
tags: tags ?? exceptionListItem.tags,
tie_breaker_id: exceptionListItem.tie_breaker_id,
type: type ?? exceptionListItem.type,
updated_by: user,
version: exceptionListItem._version ? parseInt(exceptionListItem._version, 10) : undefined,
},
{
id,
overwrite: true,
version: _version,
}
);
return transformSavedObjectUpdateToExceptionListItem({
exceptionListItem,
savedObject,
});
}
};

View file

@ -228,7 +228,7 @@ export const transformSavedObjectUpdateToExceptionListItem = ({
created_by: exceptionListItem.created_by,
description: description ?? exceptionListItem.description,
entries: entries ?? exceptionListItem.entries,
expire_time: expireTime ?? exceptionListItem.expire_time,
expire_time: expireTime,
id,
item_id: exceptionListItem.item_id,
list_id: exceptionListItem.list_id,

View file

@ -535,12 +535,16 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
newCommentValue={newComment}
newCommentOnChange={setComment}
/>
<EuiHorizontalRule />
<ExceptionsExpireTime
expireTime={expireTime}
setExpireTime={setExpireTime}
setExpireError={setExpireError}
/>
{listType !== ExceptionListTypeEnum.ENDPOINT && (
<>
<EuiHorizontalRule />
<ExceptionsExpireTime
expireTime={expireTime}
setExpireTime={setExpireTime}
setExpireError={setExpireError}
/>
</>
)}
{showAlertCloseOptions && (
<>
<EuiHorizontalRule />

View file

@ -229,7 +229,7 @@ const ExceptionsViewerComponent = ({
);
const exceptionListFilter = useMemo(() => {
if (exceptionsToShow.active && exceptionsToShow.expired) {
if (isEndpointSpecified || (exceptionsToShow.active && exceptionsToShow.expired)) {
return undefined;
}
const savedObjectPrefixes = getSavedObjectTypes({
@ -241,7 +241,7 @@ const ExceptionsViewerComponent = ({
if (exceptionsToShow.expired) {
return buildShowExpiredExceptionsFilter(savedObjectPrefixes);
}
}, [exceptionsToShow, namespaceTypes]);
}, [exceptionsToShow, namespaceTypes, isEndpointSpecified]);
const handleFetchItems = useCallback(
async (options?: GetExceptionItemProps) => {
@ -516,6 +516,7 @@ const ExceptionsViewerComponent = ({
exceptionsToShow={exceptionsToShow}
onChangeExceptionsToShow={handleExceptionsToShow}
lastUpdated={lastUpdated}
isEndpoint={isEndpointSpecified}
/>
<EuiSpacer size="m" />
<ExceptionsViewerSearchBar

View file

@ -25,6 +25,7 @@ describe('ExceptionsViewerUtility', () => {
exceptionsToShow={{ active: true }}
onChangeExceptionsToShow={(optionId: string) => {}}
lastUpdated={1660534202}
isEndpoint={false}
/>
</TestProviders>
);
@ -47,6 +48,7 @@ describe('ExceptionsViewerUtility', () => {
exceptionsToShow={{ active: true }}
onChangeExceptionsToShow={(optionId: string) => {}}
lastUpdated={Date.now()}
isEndpoint={false}
/>
</TestProviders>
);

View file

@ -42,6 +42,7 @@ interface ExceptionsViewerUtilityProps {
lastUpdated: string | number;
exceptionsToShow: { [id: string]: boolean };
onChangeExceptionsToShow: (optionId: string) => void;
isEndpoint: boolean;
}
/**
@ -52,6 +53,7 @@ const ExceptionsViewerUtilityComponent: React.FC<ExceptionsViewerUtilityProps> =
lastUpdated,
exceptionsToShow,
onChangeExceptionsToShow,
isEndpoint,
}): JSX.Element => {
return (
<MyUtilities>
@ -88,22 +90,24 @@ const ExceptionsViewerUtilityComponent: React.FC<ExceptionsViewerUtilityProps> =
/>
</EuiText>
</UtilityBarText>
<EuiButtonGroup
legend="Displayed exceptions button group"
options={[
{
id: `active`,
label: i18n.ACTIVE_EXCEPTIONS,
},
{
id: `expired`,
label: i18n.EXPIRED_EXCEPTIONS,
},
]}
idToSelectedMap={exceptionsToShow}
onChange={onChangeExceptionsToShow}
type="multi"
/>
{!isEndpoint && (
<EuiButtonGroup
legend="Displayed exceptions button group"
options={[
{
id: `active`,
label: i18n.ACTIVE_EXCEPTIONS,
},
{
id: `expired`,
label: i18n.EXPIRED_EXCEPTIONS,
},
]}
idToSelectedMap={exceptionsToShow}
onChange={onChangeExceptionsToShow}
type="multi"
/>
)}
</StyledBarGroup>
</UtilityBarSection>
</UtilityBar>

View file

@ -400,12 +400,16 @@ const EditExceptionFlyoutComponent: React.FC<EditExceptionFlyoutProps> = ({
newCommentValue={newComment}
newCommentOnChange={setComment}
/>
<EuiHorizontalRule />
<ExceptionsExpireTime
expireTime={expireTime}
setExpireTime={setExpireTime}
setExpireError={setExpireError}
/>
{listType !== ExceptionListTypeEnum.ENDPOINT && (
<>
<EuiHorizontalRule />
<ExceptionsExpireTime
expireTime={expireTime}
setExpireTime={setExpireTime}
setExpireError={setExpireError}
/>
</>
)}
{showAlertCloseOptions && (
<>
<EuiHorizontalRule />

View file

@ -66,11 +66,8 @@ export const enrichItemWithName =
*/
export const enrichItemWithExpireTime =
(expireTimeToAdd: Moment | undefined) =>
(items: ExceptionsBuilderReturnExceptionItem[]): ExceptionsBuilderReturnExceptionItem[] => {
return expireTimeToAdd != null
? enrichNewExceptionItemsWithExpireTime(items, expireTimeToAdd)
: items;
};
(items: ExceptionsBuilderReturnExceptionItem[]): ExceptionsBuilderReturnExceptionItem[] =>
enrichNewExceptionItemsWithExpireTime(items, expireTimeToAdd);
/**
* Modifies item entries to be in correct format and adds os selection to items

View file

@ -186,12 +186,13 @@ export const enrichNewExceptionItemsWithComments = (
*/
export const enrichNewExceptionItemsWithExpireTime = (
exceptionItems: ExceptionsBuilderReturnExceptionItem[],
expireTime: Moment
expireTime: Moment | undefined
): ExceptionsBuilderReturnExceptionItem[] => {
const expireTimeDateString = expireTime !== undefined ? expireTime.toISOString() : undefined;
return exceptionItems.map((item: ExceptionsBuilderReturnExceptionItem) => {
return {
...item,
expire_time: expireTime.toISOString(),
expire_time: expireTimeDateString,
};
});
};

View file

@ -124,7 +124,6 @@ const MetaRule = t.intersection([
}),
]);
// TODO: make a ticket
export const RuleSchema = t.intersection([
t.type({
author: RuleAuthorArray,

View file

@ -125,7 +125,18 @@ export const useExceptionsListCard = ({
key: 'Export',
icon: 'exportAction',
label: i18n.EXPORT_EXCEPTION_LIST,
onClick: (e: React.MouseEvent<Element, MouseEvent>) => setShowExportModal(true),
onClick: (e: React.MouseEvent<Element, MouseEvent>) => {
if (listType === ExceptionListTypeEnum.ENDPOINT) {
handleExport({
id: exceptionsList.id,
listId: exceptionsList.list_id,
namespaceType: exceptionsList.namespace_type,
includeExpiredExceptions: true,
})();
} else {
setShowExportModal(true);
}
},
},
{
key: 'Delete',
@ -158,6 +169,8 @@ export const useExceptionsListCard = ({
setShowExportModal,
listCannotBeEdited,
handleManageRules,
handleExport,
listType,
]
);

View file

@ -14,6 +14,7 @@ import {
} from '@kbn/securitysolution-exception-list-components';
import { EuiLoadingContent } from '@elastic/eui';
import { useParams } from 'react-router-dom';
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { SecurityPageName } from '../../../../common/constants';
import { SpyRoute } from '../../../common/utils/route/spy_routes';
import { ReferenceErrorModal } from '../../../detections/components/value_lists_management_flyout/reference_error_modal';
@ -67,6 +68,14 @@ export const ListsDetailViewComponent: FC = () => {
const onModalOpen = useCallback(() => setShowExportModal(true), [setShowExportModal]);
const handleExportList = useCallback(() => {
if (list?.type === ExceptionListTypeEnum.ENDPOINT) {
onExportList(true);
} else {
onModalOpen();
}
}, [onModalOpen, list, onExportList]);
const detailsViewContent = useMemo(() => {
if (viewerStatus === ViewerStatus.ERROR)
return <EmptyViewerState isReadOnly={isReadOnly} viewerStatus={viewerStatus} />;
@ -87,7 +96,7 @@ export const ListsDetailViewComponent: FC = () => {
backOptions={headerBackOptions}
securityLinkAnchorComponent={ListDetailsLinkAnchor}
onEditListDetails={onEditListDetails}
onExportList={onModalOpen}
onExportList={handleExportList}
onDeleteList={handleDelete}
onManageRules={onManageRules}
/>
@ -155,7 +164,7 @@ export const ListsDetailViewComponent: FC = () => {
handleDelete,
handleReferenceDelete,
onModalClose,
onModalOpen,
handleExportList,
]);
return (
<>