[Security Solution][Exceptions] Rule exceptions TTL - Expiration (#145180)

This commit is contained in:
Davis Plumlee 2023-02-07 16:20:39 -05:00 committed by GitHub
parent 4052d18cef
commit 92a1689e95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
109 changed files with 1343 additions and 165 deletions

View file

@ -15,7 +15,7 @@ import * as i18n from '../../translations';
interface MetaInfoDetailsProps {
label: string;
lastUpdate: JSX.Element | string;
lastUpdateValue: string;
lastUpdateValue?: string;
dataTestSubj?: string;
}
@ -42,20 +42,24 @@ export const MetaInfoDetails = memo<MetaInfoDetailsProps>(
{lastUpdate}
</EuiBadge>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" css={euiBadgeFontFamily}>
{i18n.EXCEPTION_ITEM_CARD_META_BY}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false} data-test-subj={`${dataTestSubj || ''}lastUpdateValue`}>
<EuiFlexGroup responsive gutterSize="xs" alignItems="center">
{lastUpdateValue != null && (
<>
<EuiFlexItem grow={false}>
<EuiBadge color="hollow" css={euiBadgeFontFamily}>
{lastUpdateValue}
</EuiBadge>
<EuiText size="xs" css={euiBadgeFontFamily}>
{i18n.EXCEPTION_ITEM_CARD_META_BY}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false} data-test-subj={`${dataTestSubj || ''}lastUpdateValue`}>
<EuiFlexGroup responsive gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiBadge color="hollow" css={euiBadgeFontFamily}>
{lastUpdateValue}
</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</>
)}
</EuiFlexGroup>
);
}

View file

@ -44,6 +44,12 @@ export const ExceptionItemCardMetaInfo = memo<ExceptionItemCardMetaInfoProps>(
}),
[dataTestSubj, rules, securityLinkAnchorComponent]
);
const isExpired = useMemo(
() => (item.expire_time ? new Date(item.expire_time) <= new Date() : false),
[item]
);
return (
<EuiFlexGroup alignItems="center" responsive gutterSize="s" data-test-subj={dataTestSubj}>
{FormattedDateComponent !== null && (
@ -77,6 +83,27 @@ export const ExceptionItemCardMetaInfo = memo<ExceptionItemCardMetaInfoProps>(
dataTestSubj={`${dataTestSubj || ''}UpdatedBy`}
/>
</EuiFlexItem>
{item.expire_time != null && (
<>
<EuiFlexItem css={itemCss} grow={false}>
<MetaInfoDetails
label={
isExpired
? i18n.EXCEPTION_ITEM_CARD_EXPIRED_LABEL
: i18n.EXCEPTION_ITEM_CARD_EXPIRES_LABEL
}
lastUpdate={
<FormattedDateComponent
data-test-subj={`{dataTestSubj||''}formattedDateComponentExpireTime`}
fieldName="expire_time"
value={item.expire_time}
/>
}
dataTestSubj={`${dataTestSubj || ''}ExpireTime`}
/>
</EuiFlexItem>
</>
)}
</>
)}
<EuiFlexItem>

View file

@ -34,6 +34,20 @@ export const EXCEPTION_ITEM_CARD_UPDATED_LABEL = i18n.translate(
}
);
export const EXCEPTION_ITEM_CARD_EXPIRES_LABEL = i18n.translate(
'exceptionList-components.exceptions.exceptionItem.card.expiresLabel',
{
defaultMessage: 'Expires at',
}
);
export const EXCEPTION_ITEM_CARD_EXPIRED_LABEL = i18n.translate(
'exceptionList-components.exceptions.exceptionItem.card.expiredLabel',
{
defaultMessage: 'Expired at',
}
);
export const EXCEPTION_ITEM_CARD_META_BY = i18n.translate(
'exceptionList-components.exceptions.exceptionItem.card.metaDetailsBy',
{

View file

@ -29,9 +29,9 @@ interface ExceptionListHeaderComponentProps {
canUserEditList?: boolean;
securityLinkAnchorComponent: React.ElementType; // This property needs to be removed to avoid the Prop Drilling, once we move all the common components from x-pack/security-solution/common
onEditListDetails: (listDetails: ListDetails) => void;
onExportList: () => void;
onDeleteList: () => void;
onManageRules: () => void;
onExportList: () => void;
}
export interface BackOptions {
@ -51,9 +51,9 @@ const ExceptionListHeaderComponent: FC<ExceptionListHeaderComponentProps> = ({
backOptions,
canUserEditList = true,
onEditListDetails,
onExportList,
onDeleteList,
onManageRules,
onExportList,
}) => {
const { isModalVisible, listDetails, onEdit, onSave, onCancel } = useExceptionListHeader({
name,
@ -97,9 +97,9 @@ const ExceptionListHeaderComponent: FC<ExceptionListHeaderComponentProps> = ({
isReadonly={isReadonly}
canUserEditList={canUserEditList}
securityLinkAnchorComponent={securityLinkAnchorComponent}
onExportList={onExportList}
onDeleteList={onDeleteList}
onManageRules={onManageRules}
onExportList={onExportList}
/>,
]}
breadcrumbs={[

View file

@ -18,9 +18,9 @@ interface MenuItemsProps {
linkedRules: Rule[];
canUserEditList?: boolean;
securityLinkAnchorComponent: React.ElementType; // This property needs to be removed to avoid the Prop Drilling, once we move all the common components from x-pack/security-solution/common
onExportList: () => void;
onDeleteList: () => void;
onManageRules: () => void;
onExportList: () => void;
}
const MenuItemsComponent: FC<MenuItemsProps> = ({
@ -29,9 +29,9 @@ const MenuItemsComponent: FC<MenuItemsProps> = ({
securityLinkAnchorComponent,
isReadonly,
canUserEditList = true,
onExportList,
onDeleteList,
onManageRules,
onExportList,
}) => {
const referencedLinks = useMemo(
() =>
@ -78,7 +78,7 @@ const MenuItemsComponent: FC<MenuItemsProps> = ({
data-test-subj={`${dataTestSubj || ''}ManageRulesButton`}
fill
onClick={() => {
if (typeof onExportList === 'function') onManageRules();
if (typeof onManageRules === 'function') onManageRules();
}}
>
{i18n.EXCEPTION_LIST_HEADER_MANAGE_RULES_BUTTON}

View file

@ -108,7 +108,7 @@ describe('MenuItems', () => {
fireEvent.click(wrapper.getByTestId('ManageRulesButton'));
expect(onManageRules).toHaveBeenCalled();
});
it('should call onExportList', () => {
it('should call onExportModalOpen', () => {
const wrapper = render(
<MenuItems
isReadonly={false}

View file

@ -26,6 +26,7 @@ export const getExceptionListItemSchemaMock = (
},
{ field: 'some.not.nested.field', operator: 'included', type: 'match', value: 'some value' },
],
expire_time: undefined,
id: '1',
item_id: 'endpoint_list_item',
list_id: 'endpoint_list_id',

View file

@ -0,0 +1,14 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { IsoDateString } from '@kbn/securitysolution-io-ts-types';
export const expireTime = IsoDateString;
export const expireTimeOrUndefined = t.union([expireTime, t.undefined]);
export type ExpireTimeOrUndefined = t.TypeOf<typeof expireTimeOrUndefined>;

View file

@ -0,0 +1,20 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import * as t from 'io-ts';
export const include_expired_exceptions = t.keyof({ true: null, false: null });
export const includeExpiredExceptionsOrUndefined = t.union([
include_expired_exceptions,
t.undefined,
]);
export type IncludeExpiredExceptionsOrUndefined = t.TypeOf<
typeof includeExpiredExceptionsOrUndefined
>;

View file

@ -28,6 +28,7 @@ export * from './entry_nested';
export * from './exception_export_details';
export * from './exception_list';
export * from './exception_list_item_type';
export * from './expire_time';
export * from './filter';
export * from './id';
export * from './immutable';

View file

@ -22,6 +22,7 @@ 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(
@ -39,6 +40,7 @@ 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
})
),
]);
@ -48,11 +50,12 @@ 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'
'tags' | 'item_id' | 'entries' | 'comments' | 'os_types' | 'expire_time'
> & {
comments: CreateCommentsArray;
tags: Tags;
item_id: ItemId;
entries: EntriesArray;
os_types: OsTypeArray;
expire_time: ExpireTimeOrUndefined;
};

View file

@ -25,6 +25,7 @@ import { meta } from '../../common/meta';
import { namespace_type } from '../../common/namespace_type';
import { tags } from '../../common/tags';
import { nonEmptyEntriesArray } from '../../common/non_empty_entries_array';
import { ExpireTimeOrUndefined, expireTimeOrUndefined } from '../../common';
export const createExceptionListItemSchema = t.intersection([
t.exact(
@ -39,6 +40,7 @@ export const createExceptionListItemSchema = t.intersection([
t.exact(
t.partial({
comments: DefaultCreateCommentsArray, // defaults to empty array if not set during decode
expire_time: expireTimeOrUndefined,
item_id: DefaultUuid, // defaults to GUID (uuid v4) if not set during decode
meta, // defaults to undefined if not set during decode
namespace_type, // defaults to 'single' if not set during decode
@ -53,9 +55,10 @@ export type CreateExceptionListItemSchema = t.OutputOf<typeof createExceptionLis
// This type is used after a decode since some things are defaults after a decode.
export type CreateExceptionListItemSchemaDecoded = Omit<
RequiredKeepUndefined<t.TypeOf<typeof createExceptionListItemSchema>>,
'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments'
'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments' | 'expire_time'
> & {
comments: CreateCommentsArray;
expire_time: ExpireTimeOrUndefined;
tags: Tags;
item_id: ItemId;
entries: EntriesArray;

View file

@ -25,6 +25,8 @@ import {
Tags,
tags,
name,
ExpireTimeOrUndefined,
expireTimeOrUndefined,
} from '../../common';
import { RequiredKeepUndefined } from '../../common/required_keep_undefined';
@ -46,6 +48,7 @@ export const createRuleExceptionListItemSchema = t.intersection([
namespace_type: namespaceType, // defaults to 'single' 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,
})
),
]);
@ -57,7 +60,7 @@ export type CreateRuleExceptionListItemSchema = t.OutputOf<
// This type is used after a decode since some things are defaults after a decode.
export type CreateRuleExceptionListItemSchemaDecoded = Omit<
RequiredKeepUndefined<t.TypeOf<typeof createRuleExceptionListItemSchema>>,
'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments'
'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments' | 'expire_time'
> & {
comments: CreateCommentsArray;
tags: Tags;
@ -65,4 +68,5 @@ export type CreateRuleExceptionListItemSchemaDecoded = Omit<
entries: EntriesArray;
namespace_type: NamespaceType;
os_types: OsTypeArray;
expire_time: ExpireTimeOrUndefined;
};

View file

@ -14,4 +14,5 @@ export const getExportExceptionListQuerySchemaMock = (): ExportExceptionListQuer
id: ID,
list_id: LIST_ID,
namespace_type: NAMESPACE_TYPE,
include_expired_exceptions: 'true',
});

View file

@ -45,6 +45,7 @@ describe('export_exception_list_schema', () => {
expect(message.schema).toEqual({
id: 'uuid_here',
include_expired_exceptions: 'true',
list_id: 'some-list-id',
namespace_type: 'single',
});

View file

@ -9,6 +9,7 @@
import * as t from 'io-ts';
import { id } from '../../common/id';
import { includeExpiredExceptionsOrUndefined } from '../../common/include_expired_exceptions';
import { list_id } from '../../common/list_id';
import { namespace_type } from '../../common/namespace_type';
@ -17,6 +18,7 @@ export const exportExceptionListQuerySchema = t.exact(
id,
list_id,
namespace_type,
include_expired_exceptions: includeExpiredExceptionsOrUndefined,
// TODO: Add file_name here with a default value
})
);

View file

@ -31,4 +31,5 @@ export const getImportExceptionsListItemSchemaDecodedMock = (
namespace_type: 'single',
os_types: [],
tags: [],
expire_time: undefined,
});

View file

@ -30,7 +30,7 @@ import { exceptionListItemType } from '../../common/exception_list_item_type';
import { ItemId } from '../../common/item_id';
import { EntriesArray } from '../../common/entries';
import { DefaultImportCommentsArray } from '../../common/default_import_comments_array';
import { ImportCommentsArray } from '../../common';
import { ExpireTimeOrUndefined, expireTimeOrUndefined, ImportCommentsArray } from '../../common';
/**
* Differences from this and the createExceptionsListItemSchema are
@ -67,6 +67,7 @@ export const importExceptionListItemSchema = t.intersection([
namespace_type, // defaults to 'single' 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,
})
),
]);
@ -76,7 +77,7 @@ export type ImportExceptionListItemSchema = t.OutputOf<typeof importExceptionLis
// This type is used after a decode since some things are defaults after a decode.
export type ImportExceptionListItemSchemaDecoded = Omit<
ImportExceptionListItemSchema,
'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments'
'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments' | 'expire_time'
> & {
comments: ImportCommentsArray;
tags: Tags;
@ -84,4 +85,5 @@ export type ImportExceptionListItemSchemaDecoded = Omit<
entries: EntriesArray;
namespace_type: NamespaceType;
os_types: OsTypeArray;
expire_time: ExpireTimeOrUndefined;
};

View file

@ -21,6 +21,7 @@ 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(
@ -40,6 +41,7 @@ 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,
})
),
]);
@ -49,10 +51,11 @@ 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'
'tags' | 'entries' | 'comments' | 'expire_time'
> & {
comments: UpdateCommentsArray;
tags: Tags;
entries: EntriesArray;
os_types: OsTypeArray;
expire_time: ExpireTimeOrUndefined;
};

View file

@ -22,6 +22,7 @@ import { _version } from '../../common/underscore_version';
import { id } from '../../common/id';
import { meta } from '../../common/meta';
import { namespace_type } from '../../common/namespace_type';
import { ExpireTimeOrUndefined, expireTimeOrUndefined } from '../../common';
export const updateExceptionListItemSchema = t.intersection([
t.exact(
@ -36,6 +37,7 @@ export const updateExceptionListItemSchema = t.intersection([
t.partial({
_version, // defaults to undefined if not set during decode
comments: DefaultUpdateCommentsArray, // defaults to empty array if not set during decode
expire_time: expireTimeOrUndefined,
id, // defaults to undefined if not set during decode
item_id: t.union([t.string, t.undefined]),
meta, // defaults to undefined if not set during decode
@ -51,11 +53,12 @@ export type UpdateExceptionListItemSchema = t.OutputOf<typeof updateExceptionLis
// This type is used after a decode since some things are defaults after a decode.
export type UpdateExceptionListItemSchemaDecoded = Omit<
RequiredKeepUndefined<t.TypeOf<typeof updateExceptionListItemSchema>>,
'tags' | 'entries' | 'namespace_type' | 'comments' | 'os_types'
'tags' | 'entries' | 'namespace_type' | 'comments' | 'os_types' | 'expire_time'
> & {
comments: UpdateCommentsArray;
tags: Tags;
entries: EntriesArray;
namespace_type: NamespaceType;
os_types: OsTypeArray;
expire_time: ExpireTimeOrUndefined;
};

View file

@ -34,6 +34,7 @@ export const getExceptionListItemSchemaMock = (
created_by: USER,
description: DESCRIPTION,
entries: ENTRIES,
expire_time: undefined,
id: '1',
item_id: 'endpoint_list_item',
list_id: 'endpoint_list_id',

View file

@ -26,6 +26,7 @@ import { commentsArray } from '../../common/comment';
import { entriesArray } from '../../common/entries';
import { item_id } from '../../common/item_id';
import { exceptionListItemType } from '../../common/exception_list_item_type';
import { expireTimeOrUndefined } from '../../common/expire_time';
export const exceptionListItemSchema = t.exact(
t.type({
@ -35,6 +36,7 @@ export const exceptionListItemSchema = t.exact(
created_by,
description,
entries: entriesArray,
expire_time: expireTimeOrUndefined,
id,
item_id,
list_id,

View file

@ -77,6 +77,7 @@ export interface ApiCallMemoProps {
// remove unnecessary validation checks
export interface ApiListExportProps {
id: string;
includeExpiredExceptions: boolean;
listId: string;
namespaceType: NamespaceType;
onError: (err: Error) => void;
@ -133,6 +134,7 @@ export interface ExportExceptionListProps {
id: string;
listId: string;
namespaceType: NamespaceType;
includeExpiredExceptions: boolean;
signal: AbortSignal;
}

View file

@ -532,10 +532,11 @@ const addEndpointExceptionListWithValidation = async ({
export { addEndpointExceptionListWithValidation as addEndpointExceptionList };
/**
* Fetch an ExceptionList by providing a ExceptionList ID
* Export an ExceptionList by providing a ExceptionList ID
*
* @param http Kibana http service
* @param id ExceptionList ID (not list_id)
* @param includeExpiredExceptions boolean for including expired exceptions
* @param listId ExceptionList LIST_ID (not id)
* @param namespaceType ExceptionList namespace_type
* @param signal to cancel request
@ -545,13 +546,19 @@ export { addEndpointExceptionListWithValidation as addEndpointExceptionList };
export const exportExceptionList = async ({
http,
id,
includeExpiredExceptions,
listId,
namespaceType,
signal,
}: ExportExceptionListProps): Promise<Blob> =>
http.fetch<Blob>(`${EXCEPTION_LIST_URL}/_export`, {
method: 'POST',
query: { id, list_id: listId, namespace_type: namespaceType },
query: {
id,
list_id: listId,
namespace_type: namespaceType,
include_expired_exceptions: includeExpiredExceptions,
},
signal,
});

View file

@ -34,6 +34,7 @@ export const getExceptionListItemSchemaMock = (
created_by: USER,
description: DESCRIPTION,
entries: ENTRIES,
expire_time: undefined,
id: '1',
item_id: 'endpoint_list_item',
list_id: 'endpoint_list_id',

View file

@ -112,6 +112,7 @@ export const useApi = (http: HttpStart): ExceptionsApi => {
},
async exportExceptionList({
id,
includeExpiredExceptions,
listId,
namespaceType,
onError,
@ -123,6 +124,7 @@ export const useApi = (http: HttpStart): ExceptionsApi => {
const blob = await Api.exportExceptionList({
http,
id,
includeExpiredExceptions,
listId,
namespaceType,
signal: abortCtrl.signal,

View file

@ -60,6 +60,7 @@ import {
ExceptionsBuilderReturnExceptionItem,
FormattedBuilderEntry,
OperatorOption,
SavedObjectType,
} from '../types';
export const isEntryNested = (item: BuilderEntry): item is EntryNested => {
@ -914,6 +915,21 @@ export const getDefaultNestedEmptyEntry = (): EmptyNestedEntry => ({
export const containsValueListEntry = (items: ExceptionsBuilderExceptionItem[]): boolean =>
items.some((item) => item.entries.some(({ type }) => type === OperatorTypeEnum.LIST));
export const buildShowActiveExceptionsFilter = (savedObjectPrefix: SavedObjectType[]): string => {
const now = new Date().toISOString();
const filters = savedObjectPrefix.map(
(prefix) =>
`${prefix}.attributes.expire_time > "${now}" OR NOT ${prefix}.attributes.expire_time: *`
);
return filters.join(',');
};
export const buildShowExpiredExceptionsFilter = (savedObjectPrefix: SavedObjectType[]): string => {
const now = new Date().toISOString();
const filters = savedObjectPrefix.map((prefix) => `${prefix}.attributes.expire_time <= "${now}"`);
return filters.join(',');
};
const getIndexGroupName = (indexName: string): string => {
// Check whether it is a Data Stream index
const dataStreamExp = /.ds-(.*?)-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-[0-9]{6}/;

View file

@ -87,8 +87,8 @@ describe('checking migration metadata changes on all registered SO types', () =>
"epm-packages": "1922a722ea42ab4953a96037fabb81a9ded8e240",
"epm-packages-assets": "00c8b5e5bf059627ffc9fbde920e1ac75926c5f6",
"event_loop_delays_daily": "ef49e7f15649b551b458c7ea170f3ed17f89abd0",
"exception-list": "aae42e8f19017277d194d37d4898ed6598c03e9a",
"exception-list-agnostic": "2634ee4219d27663a5755536fc06cbf3bb4beba5",
"exception-list": "38181294f64fc406c15f20d85ca306c8a4feb3c0",
"exception-list-agnostic": "d527ce9d12b134cb163150057b87529043a8ec77",
"file": "d12998f49bc82da596a9e6c8397999930187ec6a",
"file-upload-usage-collection-telemetry": "c6fcb9a7efcf19b2bb66ca6e005bfee8961f6073",
"fileShare": "f07d346acbb724eacf139a0fb781c38dc5280115",

View file

@ -53,6 +53,7 @@ export const getImportExceptionsListItemSchemaDecodedMock = (
): ImportExceptionListItemSchemaDecoded => ({
...getImportExceptionsListItemSchemaMock(itemId, listId),
comments: [],
expire_time: undefined,
meta: undefined,
namespace_type: 'single',
os_types: [],

View file

@ -33,6 +33,7 @@ export const getExceptionListItemSchemaMock = (
created_by: USER,
description: DESCRIPTION,
entries: ENTRIES,
expire_time: undefined,
id: '1',
item_id: 'endpoint_list_item',
list_id: 'endpoint_list_id',

View file

@ -698,6 +698,7 @@ describe('Exceptions Lists API', () => {
await exportExceptionList({
http: httpMock,
id: 'some-id',
includeExpiredExceptions: true,
listId: 'list-id',
namespaceType: 'single',
signal: abortCtrl.signal,
@ -707,6 +708,7 @@ describe('Exceptions Lists API', () => {
method: 'POST',
query: {
id: 'some-id',
include_expired_exceptions: true,
list_id: 'list-id',
namespace_type: 'single',
},
@ -718,6 +720,7 @@ describe('Exceptions Lists API', () => {
const exceptionResponse = await exportExceptionList({
http: httpMock,
id: 'some-id',
includeExpiredExceptions: true,
listId: 'list-id',
namespaceType: 'single',
signal: abortCtrl.signal,

View file

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

View file

@ -50,6 +50,7 @@ export const createExceptionListItemRoute = (router: ListsPluginRouter): void =>
list_id: listId,
os_types: osTypes,
type,
expire_time: expireTime,
} = request.body;
const exceptionLists = await getExceptionListClient(context);
const exceptionList = await exceptionLists.getExceptionList({
@ -92,6 +93,7 @@ export const createExceptionListItemRoute = (router: ListsPluginRouter): void =>
comments,
description,
entries,
expireTime,
itemId,
listId,
meta,

View file

@ -217,6 +217,7 @@ const updateExceptionListItems = async (
comments: listItem.comments,
description: listItem.description,
entries: remainingEntries,
expireTime: listItem.expire_time,
id: listItem.id,
itemId: listItem.item_id,
meta: listItem.meta,

View file

@ -28,11 +28,22 @@ export const exportExceptionsRoute = (router: ListsPluginRouter): void => {
const siemResponse = buildSiemResponse(response);
try {
const { id, list_id: listId, namespace_type: namespaceType } = request.query;
const {
id,
list_id: listId,
namespace_type: namespaceType,
include_expired_exceptions: includeExpiredExceptionsString,
} = request.query;
const exceptionListsClient = await getExceptionListClient(context);
// Defaults to including expired exceptions if query param is not present
const includeExpiredExceptions =
includeExpiredExceptionsString !== undefined
? includeExpiredExceptionsString === 'true'
: true;
const exportContent = await exceptionListsClient.exportExceptionListAndItems({
id,
includeExpiredExceptions,
listId,
namespaceType,
});

View file

@ -52,6 +52,11 @@ export const findExceptionListItemRoute = (router: ListsPluginRouter): void => {
body: `list_id and namespace_id need to have the same comma separated number of values. Expected list_id length: ${listId.length} to equal namespace_type length: ${namespaceType.length}`,
statusCode: 400,
});
} else if (listId.length !== filter.length && filter.length !== 0) {
return siemResponse.error({
body: `list_id and filter need to have the same comma separated number of values. Expected list_id length: ${listId.length} to equal filter length: ${filter.length}`,
statusCode: 400,
});
} else {
const exceptionListItems = await exceptionLists.findExceptionListsItem({
filter,

View file

@ -82,6 +82,7 @@ export const getExceptionFilterRoute = (router: ListsPluginRouter): void => {
excludeExceptions,
listClient,
lists: exceptionItems,
startedAt: new Date(),
});
return response.ok({ body: { filter } ?? {} });

View file

@ -49,6 +49,7 @@ 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({
@ -56,6 +57,7 @@ export const updateEndpointListItemRoute = (router: ListsPluginRouter): void =>
comments,
description,
entries,
expireTime,
id,
itemId,
meta,

View file

@ -56,6 +56,7 @@ export const updateExceptionListItemRoute = (router: ListsPluginRouter): void =>
namespace_type: namespaceType,
os_types: osTypes,
tags,
expire_time: expireTime,
} = request.body;
if (id == null && itemId == null) {
return siemResponse.error({
@ -69,6 +70,7 @@ export const updateExceptionListItemRoute = (router: ListsPluginRouter): void =>
comments,
description,
entries,
expireTime,
id,
itemId,
meta,

View file

@ -159,6 +159,9 @@ export const exceptionListItemMapping: SavedObjectsType['mappings'] = {
},
},
},
expire_time: {
type: 'date',
},
item_id: {
type: 'keyword',
},

View file

@ -22,6 +22,7 @@ const DEFAULT_EXCEPTION_LIST_SO: ExceptionListSoSchema = {
created_by: 'user',
description: 'description',
entries: undefined,
expire_time: undefined,
immutable: false,
item_id: undefined,
list_id: 'some_list',

View file

@ -14,6 +14,7 @@ import {
entriesArrayOrUndefined,
exceptionListItemType,
exceptionListType,
expireTimeOrUndefined,
immutableOrUndefined,
itemIdOrUndefined,
list_id,
@ -37,6 +38,7 @@ export const exceptionListSoSchema = t.exact(
created_by,
description,
entries: entriesArrayOrUndefined,
expire_time: expireTimeOrUndefined,
immutable: immutableOrUndefined,
item_id: itemIdOrUndefined,
list_id,

View file

@ -11,6 +11,7 @@ import type {
ExceptionListItemSchema,
} from '@kbn/securitysolution-io-ts-list-types';
import { ENTRIES } from '../../../common/constants.mock';
import {
getEntryMatchAnyExcludeMock,
getEntryMatchAnyMock,
@ -51,6 +52,7 @@ import {
buildNestedClause,
createOrClauses,
filterOutUnprocessableValueLists,
removeExpiredExceptions,
} from './build_exception_filter';
const modifiedGetEntryMatchAnyMock = (): EntryMatchAny => ({
@ -70,6 +72,7 @@ describe('build_exceptions_filter', () => {
excludeExceptions: false,
listClient,
lists: [],
startedAt: new Date(),
});
expect(filter).toBeUndefined();
});
@ -81,6 +84,7 @@ describe('build_exceptions_filter', () => {
excludeExceptions: false,
listClient,
lists: [getExceptionListItemSchemaMock()],
startedAt: new Date(),
});
expect(filter).toMatchInlineSnapshot(`
@ -154,6 +158,7 @@ describe('build_exceptions_filter', () => {
excludeExceptions: true,
listClient,
lists: [exceptionItem1, exceptionItem2],
startedAt: new Date(),
});
expect(filter).toMatchInlineSnapshot(`
Object {
@ -236,6 +241,7 @@ describe('build_exceptions_filter', () => {
excludeExceptions: true,
listClient,
lists: [exceptionItem1, exceptionItem2, exceptionItem3],
startedAt: new Date(),
});
expect(filter).toMatchInlineSnapshot(`
@ -325,6 +331,7 @@ describe('build_exceptions_filter', () => {
excludeExceptions: true,
listClient,
lists: exceptions,
startedAt: new Date(),
});
expect(filter).toMatchInlineSnapshot(`
@ -479,6 +486,95 @@ describe('build_exceptions_filter', () => {
}
`);
});
test('it should remove all exception items that are expired', async () => {
const futureDate = new Date(Date.now() + 1000000).toISOString();
const expiredDate = new Date(Date.now() - 1000000).toISOString();
const exceptions = [
{ ...getExceptionListItemSchemaMock(), entries: [ENTRIES[0]], expire_time: futureDate },
{ ...getExceptionListItemSchemaMock(), entries: [ENTRIES[1]], expire_time: expiredDate },
getExceptionListItemSchemaMock(),
];
const { filter } = await buildExceptionFilter({
alias: null,
chunkSize: 1,
excludeExceptions: true,
listClient,
lists: exceptions,
startedAt: new Date(),
});
expect(filter).toMatchInlineSnapshot(`
Object {
"meta": Object {
"alias": null,
"disabled": false,
"negate": true,
},
"query": Object {
"bool": Object {
"should": Array [
Object {
"nested": Object {
"path": "some.parentField",
"query": Object {
"bool": Object {
"minimum_should_match": 1,
"should": Array [
Object {
"match_phrase": Object {
"some.parentField.nested.field": "some value",
},
},
],
},
},
"score_mode": "none",
},
},
Object {
"bool": Object {
"filter": Array [
Object {
"nested": Object {
"path": "some.parentField",
"query": Object {
"bool": Object {
"minimum_should_match": 1,
"should": Array [
Object {
"match_phrase": Object {
"some.parentField.nested.field": "some value",
},
},
],
},
},
"score_mode": "none",
},
},
Object {
"bool": Object {
"minimum_should_match": 1,
"should": Array [
Object {
"match_phrase": Object {
"some.not.nested.field": "some value",
},
},
],
},
},
],
},
},
],
},
},
}
`);
});
});
describe('createOrClauses', () => {
@ -1297,4 +1393,22 @@ describe('build_exceptions_filter', () => {
expect(unprocessableValueListExceptions).toEqual([listExceptionItem]);
});
});
describe('removeExpiredExceptions', () => {
test('it should filter out expired exceptions', () => {
const futureDate = new Date(Date.now() + 1000000).toISOString();
const expiredDate = new Date(Date.now() - 1000000).toISOString();
const exceptions = [
{ ...getExceptionListItemSchemaMock(), expire_time: futureDate },
{ ...getExceptionListItemSchemaMock(), expire_time: expiredDate },
getExceptionListItemSchemaMock(),
];
const filteredExceptions = removeExpiredExceptions(exceptions, new Date());
expect(filteredExceptions).toEqual([
{ ...getExceptionListItemSchemaMock(), expire_time: futureDate },
getExceptionListItemSchemaMock(),
]);
});
});
});

View file

@ -273,6 +273,19 @@ export const filterOutUnprocessableValueLists = async <
return { filteredExceptions, unprocessableValueListExceptions };
};
export const removeExpiredExceptions = <
T extends ExceptionListItemSchema | CreateExceptionListItemSchema
>(
lists: T[],
startedAt: Date
): T[] =>
lists.filter((listItem) => {
if (listItem.expire_time && new Date(listItem.expire_time) < startedAt) {
return false;
}
return true;
});
export const buildExceptionFilter = async <
T extends ExceptionListItemSchema | CreateExceptionListItemSchema
>({
@ -281,17 +294,21 @@ export const buildExceptionFilter = async <
chunkSize,
alias = null,
listClient,
startedAt,
}: {
lists: T[];
excludeExceptions: boolean;
chunkSize: number;
alias: string | null;
listClient: ListClient;
startedAt: Date;
}): Promise<{ filter: Filter | undefined; unprocessedExceptions: T[] }> => {
const filteredLists = removeExpiredExceptions<T>(lists, startedAt);
// Remove exception items with large value lists. These are evaluated
// elsewhere for the moment being.
const [exceptionsWithoutValueLists, valueListExceptions] = partition(
lists,
filteredLists,
(item): item is T => !hasLargeValueList(item.entries)
);

View file

@ -41,6 +41,7 @@ export const bulkCreateExceptionListItems = async ({
created_by: user,
description: item.description,
entries: item.entries,
expire_time: item.expire_time,
immutable: false,
item_id: item.item_id,
list_id: item.list_id,

View file

@ -44,6 +44,7 @@ export const createEndpointList = async ({
created_by: user,
description: ENDPOINT_LIST_DESCRIPTION,
entries: undefined,
expire_time: undefined,
immutable: false,
item_id: undefined,
list_id: ENDPOINT_LIST_ID,

View file

@ -52,6 +52,7 @@ export const createEndpointTrustedAppsList = async ({
created_by: user,
description: ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION,
entries: undefined,
expire_time: undefined,
immutable: false,
item_id: undefined,
list_id: ENDPOINT_TRUSTED_APPS_LIST_ID,

View file

@ -62,6 +62,7 @@ export const createExceptionList = async ({
created_by: user,
description,
entries: undefined,
expire_time: undefined,
immutable,
item_id: undefined,
list_id: listId,

View file

@ -13,6 +13,7 @@ import type {
EntriesArray,
ExceptionListItemSchema,
ExceptionListItemType,
ExpireTimeOrUndefined,
ItemId,
ListId,
MetaOrUndefined,
@ -45,11 +46,13 @@ interface CreateExceptionListItemOptions {
tieBreaker?: string;
type: ExceptionListItemType;
osTypes: OsTypeArray;
expireTime: ExpireTimeOrUndefined;
}
export const createExceptionListItem = async ({
comments,
entries,
expireTime,
itemId,
listId,
savedObjectsClient,
@ -75,6 +78,7 @@ export const createExceptionListItem = async ({
created_by: user,
description,
entries,
expire_time: expireTime,
immutable: undefined,
item_id: itemId,
list_id: listId,

View file

@ -88,6 +88,7 @@ export const getCreateExceptionListItemOptionsMock = (): CreateExceptionListItem
comments,
description,
entries,
expire_time: expireTime,
item_id: itemId,
list_id: listId,
meta,
@ -102,6 +103,7 @@ export const getCreateExceptionListItemOptionsMock = (): CreateExceptionListItem
comments,
description,
entries,
expireTime,
itemId,
listId,
meta,
@ -114,14 +116,26 @@ export const getCreateExceptionListItemOptionsMock = (): CreateExceptionListItem
};
export const getUpdateExceptionListItemOptionsMock = (): UpdateExceptionListItemOptions => {
const { comments, entries, itemId, namespaceType, name, osTypes, description, meta, tags, type } =
getCreateExceptionListItemOptionsMock();
const {
comments,
entries,
expireTime,
itemId,
namespaceType,
name,
osTypes,
description,
meta,
tags,
type,
} = getCreateExceptionListItemOptionsMock();
return {
_version: undefined,
comments,
description,
entries,
expireTime,
id: ID,
itemId,
meta,
@ -161,6 +175,7 @@ export const getExceptionListSoSchemaMock = (
created_by,
description,
entries,
expire_time: undefined,
immutable: undefined,
item_id,
list_id,

View file

@ -247,6 +247,7 @@ describe('exception_list_client', () => {
(): ReturnType<ExceptionListClient['exportExceptionListAndItems']> => {
return exceptionListClient.exportExceptionListAndItems({
id: '1',
includeExpiredExceptions: true,
listId: '1',
namespaceType: 'agnostic',
});

View file

@ -287,6 +287,7 @@ export class ExceptionListClient {
comments,
description,
entries,
expireTime,
itemId,
meta,
name,
@ -300,6 +301,7 @@ export class ExceptionListClient {
comments,
description,
entries,
expireTime,
itemId,
listId: ENDPOINT_LIST_ID,
meta,
@ -356,6 +358,7 @@ export class ExceptionListClient {
comments,
description,
entries,
expireTime,
id,
itemId,
meta,
@ -371,6 +374,7 @@ export class ExceptionListClient {
comments,
description,
entries,
expireTime,
id,
itemId,
meta,
@ -526,6 +530,7 @@ export class ExceptionListClient {
comments,
description,
entries,
expireTime,
itemId,
listId,
meta,
@ -540,6 +545,7 @@ export class ExceptionListClient {
comments,
description,
entries,
expireTime,
itemId,
listId,
meta,
@ -593,6 +599,7 @@ export class ExceptionListClient {
comments,
description,
entries,
expireTime,
id,
itemId,
meta,
@ -608,6 +615,7 @@ export class ExceptionListClient {
comments,
description,
entries,
expireTime,
id,
itemId,
meta,
@ -970,6 +978,7 @@ export class ExceptionListClient {
listId,
id,
namespaceType,
includeExpiredExceptions,
}: ExportExceptionListAndItemsOptions): Promise<ExportExceptionListAndItemsReturn | null> => {
const { savedObjectsClient } = this;
@ -978,6 +987,7 @@ export class ExceptionListClient {
'exceptionsListPreExport',
{
id,
includeExpiredExceptions,
listId,
namespaceType,
},
@ -987,6 +997,7 @@ export class ExceptionListClient {
return exportExceptionListAndItems({
id,
includeExpiredExceptions,
listId,
namespaceType,
savedObjectsClient,

View file

@ -21,6 +21,7 @@ import type {
ExceptionListItemTypeOrUndefined,
ExceptionListType,
ExceptionListTypeOrUndefined,
ExpireTimeOrUndefined,
ExportExceptionDetails,
FilterOrUndefined,
FoundExceptionListItemSchema,
@ -242,6 +243,8 @@ export interface CreateExceptionListItemOptions {
comments: CreateCommentsArray;
/** an array with the exception list item entries */
entries: EntriesArray;
/** an optional datetime string with an expiration time */
expireTime: ExpireTimeOrUndefined;
/** the "item_id" of the exception list item */
itemId: ItemId;
/** the "list_id" of the parent exception list */
@ -271,6 +274,8 @@ 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 */
@ -309,6 +314,8 @@ export interface UpdateExceptionListItemOptions {
comments: UpdateCommentsArray;
/** item exception entries logic */
entries: EntriesArray;
/** an optional datetime string with an expiration time */
expireTime: ExpireTimeOrUndefined;
/** the "id" of the exception list item */
id: IdOrUndefined;
/** the "item_id" of the exception list item */
@ -340,6 +347,8 @@ 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) */
@ -490,6 +499,8 @@ export interface ExportExceptionListAndItemsOptions {
id: IdOrUndefined;
/** saved object namespace (single | agnostic) */
namespaceType: NamespaceType;
/** whether or not to include expired exceptions */
includeExpiredExceptions: boolean;
}
/**

View file

@ -29,6 +29,7 @@ describe('export_exception_list_and_items', () => {
const result = await exportExceptionListAndItems({
id: '123',
includeExpiredExceptions: true,
listId: 'non-existent',
namespaceType: 'single',
savedObjectsClient: savedObjectsClientMock.create(),
@ -45,6 +46,7 @@ describe('export_exception_list_and_items', () => {
);
const result = await exportExceptionListAndItems({
id: '123',
includeExpiredExceptions: true,
listId: 'non-existent',
namespaceType: 'single',
savedObjectsClient: savedObjectsClientMock.create(),

View file

@ -15,6 +15,7 @@ import type {
} from '@kbn/securitysolution-io-ts-list-types';
import { transformDataToNdjson } from '@kbn/securitysolution-utils';
import type { SavedObjectsClientContract } from '@kbn/core/server';
import { getSavedObjectType } from '@kbn/securitysolution-list-utils';
import { findExceptionListItemPointInTimeFinder } from './find_exception_list_item_point_in_time_finder';
import { getExceptionList } from './get_exception_list';
@ -24,6 +25,7 @@ interface ExportExceptionListAndItemsOptions {
listId: ListIdOrUndefined;
savedObjectsClient: SavedObjectsClientContract;
namespaceType: NamespaceType;
includeExpiredExceptions: boolean;
}
export interface ExportExceptionListAndItemsReturn {
@ -35,6 +37,7 @@ export const exportExceptionListAndItems = async ({
id,
listId,
namespaceType,
includeExpiredExceptions,
savedObjectsClient,
}: ExportExceptionListAndItemsOptions): Promise<ExportExceptionListAndItemsReturn | null> => {
const exceptionList = await getExceptionList({
@ -52,10 +55,14 @@ export const exportExceptionListAndItems = async ({
const executeFunctionOnStream = (response: FoundExceptionListItemSchema): void => {
exceptionItems = [...exceptionItems, ...response.data];
};
const savedObjectPrefix = getSavedObjectType({ namespaceType });
const filter = includeExpiredExceptions
? undefined
: `(${savedObjectPrefix}.attributes.expire_time > "${new Date().toISOString()}" OR NOT ${savedObjectPrefix}.attributes.expire_time: *)`;
await findExceptionListItemPointInTimeFinder({
executeFunctionOnStream,
filter: undefined,
filter,
listId: exceptionList.list_id,
maxSize: undefined, // NOTE: This is unbounded when it is "undefined"
namespaceType: exceptionList.namespace_type,

View file

@ -11,6 +11,7 @@ import type {
EntriesArray,
ExceptionListItemSchema,
ExceptionListItemTypeOrUndefined,
ExpireTimeOrUndefined,
IdOrUndefined,
ItemIdOrUndefined,
MetaOrUndefined,
@ -38,6 +39,7 @@ interface UpdateExceptionListItemOptions {
name: NameOrUndefined;
description: DescriptionOrUndefined;
entries: EntriesArray;
expireTime: ExpireTimeOrUndefined;
savedObjectsClient: SavedObjectsClientContract;
namespaceType: NamespaceType;
osTypes: OsTypeArray;
@ -53,6 +55,7 @@ export const updateExceptionListItem = async ({
_version,
comments,
entries,
expireTime,
id,
savedObjectsClient,
namespaceType,
@ -87,6 +90,7 @@ export const updateExceptionListItem = async ({
comments: transformedComments,
description,
entries,
expire_time: expireTime,
meta,
name,
os_types: osTypes,

View file

@ -39,7 +39,7 @@ describe('getExceptionListsItemFilter', () => {
savedObjectType: ['exception-list'],
});
expect(filter).toEqual(
'((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "some-list-id") AND exception-list.attributes.name: "Sample Endpoint Exception List")'
'((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "some-list-id") AND (exception-list.attributes.name: "Sample Endpoint Exception List"))'
);
});
@ -61,7 +61,7 @@ describe('getExceptionListsItemFilter', () => {
savedObjectType: ['exception-list', 'exception-list-agnostic'],
});
expect(filter).toEqual(
'((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "list-1") AND exception-list.attributes.name: "Sample Endpoint Exception List") OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-2")'
'((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "list-1") AND (exception-list.attributes.name: "Sample Endpoint Exception List")) OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-2")'
);
});
@ -83,7 +83,7 @@ describe('getExceptionListsItemFilter', () => {
savedObjectType: ['exception-list', 'exception-list-agnostic', 'exception-list-agnostic'],
});
expect(filter).toEqual(
'((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "list-1") AND exception-list.attributes.name: "Sample Endpoint Exception List") OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-2") OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-3")'
'((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "list-1") AND (exception-list.attributes.name: "Sample Endpoint Exception List")) OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-2") OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-3")'
);
});
@ -98,7 +98,7 @@ describe('getExceptionListsItemFilter', () => {
savedObjectType: ['exception-list', 'exception-list-agnostic', 'exception-list-agnostic'],
});
expect(filter).toEqual(
'((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "list-1") AND exception-list.attributes.name: "Sample Endpoint Exception List 1") OR ((exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-2") AND exception-list.attributes.name: "Sample Endpoint Exception List 2") OR ((exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-3") AND exception-list.attributes.name: "Sample Endpoint Exception List 3")'
'((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "list-1") AND (exception-list.attributes.name: "Sample Endpoint Exception List 1")) OR ((exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-2") AND (exception-list.attributes.name: "Sample Endpoint Exception List 2")) OR ((exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-3") AND (exception-list.attributes.name: "Sample Endpoint Exception List 3"))'
);
});
});

View file

@ -26,7 +26,7 @@ export const getExceptionListsItemFilter = ({
const escapedListId = escapeQuotes(singleListId);
const listItemAppend = `(${savedObjectType[index]}.attributes.list_type: item AND ${savedObjectType[index]}.attributes.list_id: "${escapedListId}")`;
const listItemAppendWithFilter =
filter[index] != null ? `(${listItemAppend} AND ${filter[index]})` : listItemAppend;
filter[index] != null ? `(${listItemAppend} AND (${filter[index]}))` : listItemAppend;
if (accum === '') {
return listItemAppendWithFilter;
} else {

View file

@ -22,6 +22,7 @@ describe('bulkCreateImportedItems', () => {
created_by: 'elastic',
description: 'description here',
entries: ENTRIES,
expire_time: undefined,
immutable: undefined,
item_id: 'item-id',
list_id: 'list-id',

View file

@ -21,6 +21,7 @@ describe('bulkCreateImportedLists', () => {
created_by: 'elastic',
description: 'some description',
entries: undefined,
expire_time: undefined,
immutable: false,
item_id: undefined,
list_id: 'list-id',

View file

@ -65,7 +65,7 @@ describe('find_all_exception_list_item_types', () => {
expect(savedObjectsClient.find).toHaveBeenCalledWith({
filter:
'((exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "detection_list_id") AND exception-list-agnostic.attributes.item_id:(1))',
'((exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "detection_list_id") AND (exception-list-agnostic.attributes.item_id:(1)))',
page: undefined,
perPage: 100,
sortField: undefined,
@ -83,7 +83,7 @@ describe('find_all_exception_list_item_types', () => {
expect(savedObjectsClient.find).toHaveBeenCalledWith({
filter:
'((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "detection_list_id") AND exception-list.attributes.item_id:(1))',
'((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "detection_list_id") AND (exception-list.attributes.item_id:(1)))',
page: undefined,
perPage: 100,
sortField: undefined,
@ -101,7 +101,7 @@ describe('find_all_exception_list_item_types', () => {
expect(savedObjectsClient.find).toHaveBeenCalledWith({
filter:
'((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "detection_list_id") AND exception-list.attributes.item_id:(2)) OR ((exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "detection_list_id") AND exception-list-agnostic.attributes.item_id:(1))',
'((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "detection_list_id") AND (exception-list.attributes.item_id:(2))) OR ((exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "detection_list_id") AND (exception-list-agnostic.attributes.item_id:(1)))',
page: undefined,
perPage: 100,
sortField: undefined,

View file

@ -50,6 +50,7 @@ export const sortExceptionItemsToUpdateOrCreate = ({
comments,
description,
entries,
expire_time: expireTime,
item_id: itemId,
meta,
list_id: listId,
@ -89,6 +90,7 @@ export const sortExceptionItemsToUpdateOrCreate = ({
created_by: user,
description,
entries,
expire_time: expireTime,
immutable: undefined,
item_id: itemId,
list_id: listId,

View file

@ -71,6 +71,7 @@ export const sortExceptionListsToUpdateOrCreate = ({
created_by: user,
description,
entries: undefined,
expire_time: undefined,
immutable: false,
item_id: undefined,
list_id: listId,
@ -112,6 +113,7 @@ export const sortExceptionListsToUpdateOrCreate = ({
created_by: user,
description,
entries: undefined,
expire_time: undefined,
immutable: false,
item_id: undefined,
list_type: 'list',

View file

@ -150,6 +150,7 @@ export const transformSavedObjectToExceptionListItem = ({
created_by,
description,
entries,
expire_time,
item_id: itemId,
list_id,
meta,
@ -174,6 +175,7 @@ export const transformSavedObjectToExceptionListItem = ({
created_by,
description,
entries: entries ?? [],
expire_time,
id,
item_id: itemId ?? '(unknown)',
list_id,
@ -203,6 +205,7 @@ export const transformSavedObjectUpdateToExceptionListItem = ({
comments,
description,
entries,
expire_time: expireTime,
meta,
name,
os_types: osTypes,
@ -225,6 +228,7 @@ export const transformSavedObjectUpdateToExceptionListItem = ({
created_by: exceptionListItem.created_by,
description: description ?? exceptionListItem.description,
entries: entries ?? exceptionListItem.entries,
expire_time: expireTime ?? exceptionListItem.expire_time,
id,
item_id: exceptionListItem.item_id,
list_id: exceptionListItem.list_id,
@ -308,6 +312,7 @@ export const transformCreateCommentsToComments = ({
};
export const transformCreateExceptionListItemOptionsToCreateExceptionListItemSchema = ({
expireTime,
listId,
itemId,
namespaceType,
@ -316,6 +321,7 @@ export const transformCreateExceptionListItemOptionsToCreateExceptionListItemSch
}: CreateExceptionListItemOptions): CreateExceptionListItemSchema => {
return {
...rest,
expire_time: expireTime,
item_id: itemId,
list_id: listId,
namespace_type: namespaceType,
@ -327,6 +333,7 @@ export const transformUpdateExceptionListItemOptionsToUpdateExceptionListItemSch
itemId,
namespaceType,
osTypes,
expireTime,
// The `UpdateExceptionListItemOptions` type differs from the schema in that some properties are
// marked as having `undefined` as a valid value, where the schema, however, requires it.
// So we assign defaults here
@ -338,6 +345,7 @@ export const transformUpdateExceptionListItemOptionsToUpdateExceptionListItemSch
return {
...rest,
description,
expire_time: expireTime,
item_id: itemId,
name,
namespace_type: namespaceType,

View file

@ -30,14 +30,14 @@ type NonNullableTypeProperties<T> = {
* create a value for (almost) all properties
*/
type CreateExceptionListItemSchemaWithNonNullProps = NonNullableTypeProperties<
Omit<CreateExceptionListItemSchema, 'meta'>
Omit<CreateExceptionListItemSchema, 'meta' | 'expire_time'>
> &
Pick<CreateExceptionListItemSchema, 'meta'>;
Pick<CreateExceptionListItemSchema, 'meta' | 'expire_time'>;
type UpdateExceptionListItemSchemaWithNonNullProps = NonNullableTypeProperties<
Omit<UpdateExceptionListItemSchema, 'meta'>
Omit<UpdateExceptionListItemSchema, 'meta' | 'expire_time'>
> &
Pick<UpdateExceptionListItemSchema, 'meta'>;
Pick<UpdateExceptionListItemSchema, 'meta' | 'expire_time'>;
const exceptionItemToCreateExceptionItem = (
exceptionItem: ExceptionListItemSchema
@ -46,6 +46,7 @@ const exceptionItemToCreateExceptionItem = (
/* eslint-disable @typescript-eslint/naming-convention */
description,
entries,
expire_time,
list_id,
name,
type,
@ -61,6 +62,7 @@ const exceptionItemToCreateExceptionItem = (
return {
description,
entries,
expire_time,
list_id,
name,
type,
@ -109,6 +111,7 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator<ExceptionList
value: '741462ab431a22233C787BAAB9B653C7',
},
],
expire_time: undefined,
id: this.seededUUIDv4(),
item_id: this.seededUUIDv4(),
list_id: 'endpoint_list_id',

View file

@ -46,9 +46,11 @@ export const EXCEPTIONS_TABLE_SHOWING_LISTS = '[data-test-subj="showingException
export const EXCEPTIONS_TABLE_DELETE_BTN =
'[data-test-subj="sharedListOverflowCardActionItemDelete"]';
export const EXCEPTIONS_TABLE_EXPORT_BTN =
export const EXCEPTIONS_TABLE_EXPORT_MODAL_BTN =
'[data-test-subj="sharedListOverflowCardActionItemExport"]';
export const EXCEPTIONS_TABLE_EXPORT_CONFIRM_BTN = '[data-test-subj="confirmModalConfirmButton"]';
export const EXCEPTIONS_TABLE_SEARCH_CLEAR =
'[data-test-subj="allExceptionListsPanel"] button.euiFormControlLayoutClearButton';

View file

@ -12,8 +12,9 @@ import {
EXCEPTIONS_TABLE_SEARCH_CLEAR,
EXCEPTIONS_TABLE_MODAL,
EXCEPTIONS_TABLE_MODAL_CONFIRM_BTN,
EXCEPTIONS_TABLE_EXPORT_BTN,
EXCEPTIONS_TABLE_EXPORT_MODAL_BTN,
EXCEPTIONS_OVERFLOW_ACTIONS_BTN,
EXCEPTIONS_TABLE_EXPORT_CONFIRM_BTN,
} from '../screens/exceptions';
export const clearSearchSelection = () => {
@ -26,7 +27,8 @@ export const expandExceptionActions = () => {
export const exportExceptionList = () => {
cy.get(EXCEPTIONS_OVERFLOW_ACTIONS_BTN).first().click();
cy.get(EXCEPTIONS_TABLE_EXPORT_BTN).first().click();
cy.get(EXCEPTIONS_TABLE_EXPORT_MODAL_BTN).first().click();
cy.get(EXCEPTIONS_TABLE_EXPORT_CONFIRM_BTN).first().click();
};
export const deleteExceptionListWithoutRuleReference = () => {

View file

@ -112,9 +112,9 @@ export const BarGroup = styled.div.attrs({
`;
BarGroup.displayName = 'BarGroup';
export const BarText = styled.p.attrs({
className: 'siemUtilityBar__text',
})<{ shouldWrap: boolean }>`
export const BarText = styled.p.attrs(({ className }) => ({
className: className || 'siemUtilityBar__text',
}))<{ shouldWrap: boolean }>`
${({ shouldWrap, theme }) => css`
color: ${theme.eui.euiTextSubduedColor};
font-size: ${theme.eui.euiFontSizeXS};

View file

@ -13,11 +13,12 @@ export interface UtilityBarTextProps {
children: string | JSX.Element;
dataTestSubj?: string;
shouldWrap?: boolean;
className?: string;
}
export const UtilityBarText = React.memo<UtilityBarTextProps>(
({ children, dataTestSubj, shouldWrap = false }) => (
<BarText data-test-subj={dataTestSubj} shouldWrap={shouldWrap}>
({ children, dataTestSubj, shouldWrap = false, className }) => (
<BarText data-test-subj={dataTestSubj} shouldWrap={shouldWrap} className={className}>
{children}
</BarText>
)

View file

@ -33,6 +33,7 @@ import type {
ExceptionsBuilderReturnExceptionItem,
} from '@kbn/securitysolution-list-utils';
import type { Moment } from 'moment';
import type { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
import * as i18n from './translations';
import { ExceptionItemComments } from '../item_comments';
@ -54,6 +55,7 @@ import { enrichNewExceptionItems } from '../flyout_components/utils';
import { useCloseAlertsFromExceptions } from '../../logic/use_close_alerts';
import { ruleTypesThatAllowLargeValueLists } from '../../utils/constants';
import { useInvalidateFetchRuleByIdQuery } from '../../../rule_management/api/hooks/use_fetch_rule_by_id_query';
import { ExceptionsExpireTime } from '../flyout_components/expire_time';
const SectionHeader = styled(EuiTitle)`
${() => css`
@ -153,6 +155,8 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
newComment,
itemConditionValidationErrorExists,
errorSubmitting,
expireTime,
expireErrorExists,
},
dispatch,
] = useReducer(createExceptionItemsReducer(), {
@ -312,6 +316,26 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
[dispatch]
);
const setExpireTime = useCallback(
(exceptionExpireTime: Moment | undefined): void => {
dispatch({
type: 'setExpireTime',
expireTime: exceptionExpireTime,
});
},
[dispatch]
);
const setExpireError = useCallback(
(errorExists: boolean): void => {
dispatch({
type: 'setExpireError',
errorExists,
});
},
[dispatch]
);
useEffect((): void => {
if (listType === ExceptionListTypeEnum.ENDPOINT && alertData != null) {
setInitialExceptionItems(
@ -343,6 +367,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
sharedLists,
listType,
selectedOs: osTypesSelection,
expireTime,
items: exceptionItems,
});
@ -391,6 +416,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
bulkCloseIndex,
setErrorSubmitting,
invalidateFetchRuleByIdQuery,
expireTime,
]);
const isSubmitButtonDisabled = useMemo(
@ -401,6 +427,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
exceptionItemName.trim() === '' ||
exceptionItems.every((item) => item.entries.length === 0) ||
itemConditionValidationErrorExists ||
expireErrorExists ||
(addExceptionToRadioSelection === 'add_to_lists' && isEmpty(exceptionListsToAddTo)),
[
isSubmitting,
@ -411,6 +438,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
itemConditionValidationErrorExists,
addExceptionToRadioSelection,
exceptionListsToAddTo,
expireErrorExists,
]
);
@ -502,6 +530,12 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
newCommentValue={newComment}
newCommentOnChange={setComment}
/>
<EuiHorizontalRule />
<ExceptionsExpireTime
expireTime={expireTime}
setExpireTime={setExpireTime}
setExpireError={setExpireError}
/>
{showAlertCloseOptions && (
<>
<EuiHorizontalRule />

View file

@ -11,6 +11,7 @@ import type {
ExceptionsBuilderExceptionItem,
ExceptionsBuilderReturnExceptionItem,
} from '@kbn/securitysolution-list-utils';
import type { Moment } from 'moment';
import type { Rule } from '../../../rule_management/logic/types';
@ -30,6 +31,8 @@ export interface State {
exceptionListsToAddTo: ExceptionListSchema[];
selectedRulesToAddTo: Rule[];
errorSubmitting: Error | null;
expireTime: Moment | undefined;
expireErrorExists: boolean;
}
export const initialState: State = {
@ -48,6 +51,8 @@ export const initialState: State = {
selectedRulesToAddTo: [],
listType: ExceptionListTypeEnum.RULE_DEFAULT,
errorSubmitting: null,
expireTime: undefined,
expireErrorExists: false,
};
export type Action =
@ -110,6 +115,14 @@ export type Action =
| {
type: 'setErrorSubmitting';
err: Error | null;
}
| {
type: 'setExpireTime';
expireTime: Moment | undefined;
}
| {
type: 'setExpireError';
errorExists: boolean;
};
export const createExceptionItemsReducer =
@ -244,6 +257,22 @@ export const createExceptionItemsReducer =
errorSubmitting: err,
};
}
case 'setExpireTime': {
const { expireTime } = action;
return {
...state,
expireTime,
};
}
case 'setExpireError': {
const { errorExists } = action;
return {
...state,
expireErrorExists: errorExists,
};
}
default:
return state;
}

View file

@ -10,6 +10,8 @@ import { mount, shallow } from 'enzyme';
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { fetchExceptionListsItemsByListIds } from '@kbn/securitysolution-list-api';
import { ExceptionsViewer } from '.';
import { useKibana } from '../../../../common/lib/kibana';
import { TestProviders } from '../../../../common/mock';
@ -20,6 +22,7 @@ import * as i18n from './translations';
jest.mock('../../../../common/lib/kibana');
jest.mock('@kbn/securitysolution-list-hooks');
jest.mock('@kbn/securitysolution-list-api');
jest.mock('../../logic/use_find_references');
jest.mock('react', () => {
const r = jest.requireActual('react');
@ -78,6 +81,8 @@ describe('ExceptionsViewer', () => {
},
});
(fetchExceptionListsItemsByListIds as jest.Mock).mockReturnValue({ total: 0 });
(useFindExceptionListReferences as jest.Mock).mockReturnValue([
false,
false,
@ -130,6 +135,7 @@ describe('ExceptionsViewer', () => {
exceptionToEdit: null,
viewerState: 'loading',
exceptionLists: [],
exceptionsToShow: { active: true },
},
jest.fn(),
]);
@ -168,6 +174,7 @@ describe('ExceptionsViewer', () => {
exceptionToEdit: null,
viewerState: 'empty_search',
exceptionLists: [],
exceptionsToShow: { active: true },
},
jest.fn(),
]);
@ -206,6 +213,7 @@ describe('ExceptionsViewer', () => {
exceptionToEdit: null,
viewerState: 'empty',
exceptionLists: [],
exceptionsToShow: { active: true },
},
jest.fn(),
]);
@ -250,6 +258,7 @@ describe('ExceptionsViewer', () => {
exceptionToEdit: null,
viewerState: 'empty',
exceptionLists: [],
exceptionsToShow: { active: true },
},
jest.fn(),
]);
@ -294,6 +303,7 @@ describe('ExceptionsViewer', () => {
exceptionToEdit: null,
viewerState: null,
exceptionLists: [],
exceptionsToShow: { active: true },
},
jest.fn(),
]);
@ -328,6 +338,7 @@ describe('ExceptionsViewer', () => {
exceptionToEdit: sampleExceptionItem,
viewerState: null,
exceptionLists: [],
exceptionsToShow: { active: true },
},
jest.fn(),
]);

View file

@ -24,6 +24,11 @@ import {
fetchExceptionListsItemsByListIds,
} from '@kbn/securitysolution-list-api';
import {
buildShowActiveExceptionsFilter,
buildShowExpiredExceptionsFilter,
getSavedObjectTypes,
} from '@kbn/securitysolution-list-utils';
import { useUserData } from '../../../../detections/components/user_info';
import { useKibana, useToasts } from '../../../../common/lib/kibana';
import { ExceptionsViewerSearchBar } from './search_bar';
@ -44,7 +49,7 @@ const StyledText = styled(EuiText)`
font-style: italic;
`;
const STATES_SEARCH_HIDDEN: ViewerState[] = ['error', 'empty'];
const STATES_FILTERS_HIDDEN: ViewerState[] = ['error'];
const STATES_PAGINATION_UTILITY_HIDDEN: ViewerState[] = [
'loading',
'empty_search',
@ -66,6 +71,7 @@ const initialState: State = {
viewerState: 'loading',
isReadOnly: true,
lastUpdated: Date.now(),
exceptionsToShow: { active: true },
};
export interface GetExceptionItemProps {
@ -116,7 +122,16 @@ const ExceptionsViewerComponent = ({
// Reducer state
const [
{ exceptions, pagination, currenFlyout, exceptionToEdit, viewerState, isReadOnly, lastUpdated },
{
exceptions,
pagination,
currenFlyout,
exceptionToEdit,
viewerState,
isReadOnly,
lastUpdated,
exceptionsToShow,
},
dispatch,
] = useReducer(allExceptionItemsReducer(), {
...initialState,
@ -179,6 +194,16 @@ const ExceptionsViewerComponent = ({
[dispatch]
);
const setExceptionsToShow = useCallback(
(optionId: string): void => {
dispatch({
type: 'setExceptionsToShow',
optionId,
});
},
[dispatch]
);
const [isLoadingReferences, isFetchReferencesError, allReferences, fetchReferences] =
useFindExceptionListReferences();
@ -198,6 +223,26 @@ const ExceptionsViewerComponent = ({
}
}, [isLoadingReferences, isFetchReferencesError, setViewerState, viewerState]);
const namespaceTypes = useMemo(
() => exceptionListsToQuery.map((list) => list.namespace_type),
[exceptionListsToQuery]
);
const exceptionListFilter = useMemo(() => {
if (exceptionsToShow.active && exceptionsToShow.expired) {
return undefined;
}
const savedObjectPrefixes = getSavedObjectTypes({
namespaceType: namespaceTypes,
});
if (exceptionsToShow.active) {
return buildShowActiveExceptionsFilter(savedObjectPrefixes);
}
if (exceptionsToShow.expired) {
return buildShowExpiredExceptionsFilter(savedObjectPrefixes);
}
}, [exceptionsToShow, namespaceTypes]);
const handleFetchItems = useCallback(
async (options?: GetExceptionItemProps) => {
const abortCtrl = new AbortController();
@ -228,10 +273,10 @@ const ExceptionsViewerComponent = ({
total,
data,
} = await fetchExceptionListsItemsByListIds({
filter: undefined,
filter: exceptionListFilter,
http: services.http,
listIds: exceptionListsToQuery.map((list) => list.list_id),
namespaceTypes: exceptionListsToQuery.map((list) => list.namespace_type),
namespaceTypes,
search: options?.search,
pagination: newPagination,
signal: abortCtrl.signal,
@ -248,9 +293,34 @@ const ExceptionsViewerComponent = ({
total,
};
},
[pagination.pageIndex, pagination.pageSize, exceptionListsToQuery, services.http]
[
pagination.pageIndex,
pagination.pageSize,
exceptionListsToQuery,
services.http,
exceptionListFilter,
namespaceTypes,
]
);
const getTotalExceptionCount = useCallback(async () => {
const abortCtrl = new AbortController();
if (exceptionListsToQuery.length === 0) {
return 0;
}
const { total } = await fetchExceptionListsItemsByListIds({
filter: undefined,
http: services.http,
listIds: exceptionListsToQuery.map((list) => list.list_id),
namespaceTypes,
pagination: {},
signal: abortCtrl.signal,
});
return total;
}, [exceptionListsToQuery, namespaceTypes, services.http]);
const handleGetExceptionListItems = useCallback(
async (options?: GetExceptionItemProps) => {
try {
@ -266,7 +336,9 @@ const ExceptionsViewerComponent = ({
},
});
setViewerState(total > 0 ? null : 'empty');
setViewerState(
total > 0 ? null : (await getTotalExceptionCount()) > 0 ? 'empty_search' : 'empty'
);
} catch (e) {
setViewerState('error');
@ -276,7 +348,7 @@ const ExceptionsViewerComponent = ({
});
}
},
[handleFetchItems, setExceptions, setViewerState, toasts]
[handleFetchItems, setExceptions, setViewerState, toasts, getTotalExceptionCount]
);
const handleSearch = useCallback(
@ -306,6 +378,13 @@ const ExceptionsViewerComponent = ({
[handleFetchItems, setExceptions, setViewerState, toasts]
);
const handleExceptionsToShow = useCallback(
(optionId: string): void => {
setExceptionsToShow(optionId);
},
[setExceptionsToShow]
);
const handleAddException = useCallback((): void => {
setFlyoutType('addException');
}, [setFlyoutType]);
@ -430,22 +509,25 @@ const ExceptionsViewerComponent = ({
{isEndpointSpecified ? i18n.ENDPOINT_EXCEPTIONS_TAB_ABOUT : i18n.EXCEPTIONS_TAB_ABOUT}
</StyledText>
<EuiSpacer size="l" />
{!STATES_SEARCH_HIDDEN.includes(viewerState) && (
<ExceptionsViewerSearchBar
canAddException={isReadOnly}
isEndpoint={isEndpointSpecified}
isSearching={viewerState === 'searching'}
onSearch={handleSearch}
onAddExceptionClick={handleAddException}
/>
)}
{!STATES_PAGINATION_UTILITY_HIDDEN.includes(viewerState) && (
{!STATES_FILTERS_HIDDEN.includes(viewerState) && (
<>
<EuiSpacer size="l" />
<ExceptionsViewerUtility pagination={pagination} lastUpdated={lastUpdated} />
<ExceptionsViewerUtility
pagination={pagination}
exceptionsToShow={exceptionsToShow}
onChangeExceptionsToShow={handleExceptionsToShow}
lastUpdated={lastUpdated}
/>
<EuiSpacer size="m" />
<ExceptionsViewerSearchBar
canAddException={isReadOnly}
isEndpoint={isEndpointSpecified}
isSearching={viewerState === 'searching'}
onSearch={handleSearch}
onAddExceptionClick={handleAddException}
/>
</>
)}
<EuiSpacer size="l" />
<ExceptionsViewerItems
isReadOnly={isReadOnly}

View file

@ -29,6 +29,7 @@ export interface State {
viewerState: ViewerState;
isReadOnly: boolean;
lastUpdated: string | number;
exceptionsToShow: { [id: string]: boolean };
}
export type Action =
@ -53,6 +54,10 @@ export type Action =
| {
type: 'setLastUpdateTime';
lastUpdate: string | number;
}
| {
type: 'setExceptionsToShow';
optionId: string;
};
export const allExceptionItemsReducer =
@ -104,6 +109,21 @@ export const allExceptionItemsReducer =
lastUpdated: action.lastUpdate,
};
}
case 'setExceptionsToShow': {
const newExceptionsToShow = {
...state.exceptionsToShow,
...{ [action.optionId]: !state.exceptionsToShow[action.optionId] },
};
// At least one button must be selected
if (!newExceptionsToShow.active && !newExceptionsToShow.expired) {
return { ...state, exceptionsToShow: { active: true } };
}
return {
...state,
exceptionsToShow: newExceptionsToShow,
};
}
default:
return state;
}

View file

@ -150,3 +150,17 @@ export const ADD_TO_DETECTIONS_LIST = i18n.translate(
defaultMessage: 'Add rule exception',
}
);
export const ACTIVE_EXCEPTIONS = i18n.translate(
'xpack.securitySolution.ruleExceptions.allExceptionItems.activeDetectionsLabel',
{
defaultMessage: 'Active exceptions',
}
);
export const EXPIRED_EXCEPTIONS = i18n.translate(
'xpack.securitySolution.ruleExceptions.allExceptionItems.expiredDetectionsLabel',
{
defaultMessage: 'Expired exceptions',
}
);

View file

@ -22,6 +22,8 @@ describe('ExceptionsViewerUtility', () => {
totalItemCount: 105,
pageSizeOptions: [5, 10, 20, 50, 100],
}}
exceptionsToShow={{ active: true }}
onChangeExceptionsToShow={(optionId: string) => {}}
lastUpdated={1660534202}
/>
</TestProviders>
@ -42,6 +44,8 @@ describe('ExceptionsViewerUtility', () => {
totalItemCount: 1,
pageSizeOptions: [5, 10, 20, 50, 100],
}}
exceptionsToShow={{ active: true }}
onChangeExceptionsToShow={(optionId: string) => {}}
lastUpdated={Date.now()}
/>
</TestProviders>

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiText, EuiButtonGroup, EuiFlexGroup } from '@elastic/eui';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n-react';
@ -18,24 +18,30 @@ import {
UtilityBarText,
} from '../../../../common/components/utility_bar';
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';
import * as i18n from './translations';
const StyledText = styled.span`
font-weight: bold;
`;
const MyUtilities = styled(EuiFlexGroup)`
const MyUtilities = styled.div`
height: 50px;
`;
const StyledCondition = styled.span`
display: inline-block !important;
vertical-align: middle !important;
const StyledBarGroup = styled(EuiFlexGroup)`
align-items: center;
`;
const PaginationUtilityBarText = styled(UtilityBarText)`
align-self: center;
`;
interface ExceptionsViewerUtilityProps {
pagination: ExceptionsPagination;
// Corresponds to last time exception items were fetched
lastUpdated: string | number;
exceptionsToShow: { [id: string]: boolean };
onChangeExceptionsToShow: (optionId: string) => void;
}
/**
@ -44,19 +50,21 @@ interface ExceptionsViewerUtilityProps {
const ExceptionsViewerUtilityComponent: React.FC<ExceptionsViewerUtilityProps> = ({
pagination,
lastUpdated,
}): JSX.Element => (
<MyUtilities alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
exceptionsToShow,
onChangeExceptionsToShow,
}): JSX.Element => {
return (
<MyUtilities>
<UtilityBar>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText dataTestSubj="exceptionsShowing">
<PaginationUtilityBarText dataTestSubj="exceptionsShowing">
<FormattedMessage
id="xpack.securitySolution.exceptions.viewer.paginationDetails"
defaultMessage="Showing {partOne} of {partTwo}"
values={{
partOne: (
<StyledText>{`1-${Math.min(
<StyledText>{`${pagination.totalItemCount === 0 ? '0' : '1'}-${Math.min(
pagination.pageSize,
pagination.totalItemCount
)}`}</StyledText>
@ -64,31 +72,44 @@ const ExceptionsViewerUtilityComponent: React.FC<ExceptionsViewerUtilityProps> =
partTwo: <StyledText>{`${pagination.totalItemCount}`}</StyledText>,
}}
/>
</UtilityBarText>
</PaginationUtilityBarText>
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" data-test-subj="exceptionsViewerLastUpdated">
<FormattedMessage
id="xpack.securitySolution.exceptions.viewer.lastUpdated"
defaultMessage="Updated {updated}"
values={{
updated: (
<StyledCondition>
<FormattedRelativePreferenceDate
value={lastUpdated}
tooltipAnchorClassName="eui-textTruncate"
<UtilityBarSection>
<StyledBarGroup>
<UtilityBarText dataTestSubj="lastUpdated">
<EuiText size="s" data-test-subj="exceptionsViewerLastUpdated">
<FormattedMessage
id="xpack.securitySolution.exceptions.viewer.lastUpdated"
defaultMessage="Updated {updated}"
values={{
updated: <FormattedRelativePreferenceDate value={lastUpdated} />,
}}
/>
</StyledCondition>
),
}}
/>
</EuiText>
</EuiFlexItem>
</MyUtilities>
);
</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"
/>
</StyledBarGroup>
</UtilityBarSection>
</UtilityBar>
</MyUtilities>
);
};
ExceptionsViewerUtilityComponent.displayName = 'ExceptionsViewerUtilityComponent';

View file

@ -33,6 +33,8 @@ import {
import type { ExceptionsBuilderReturnExceptionItem } from '@kbn/securitysolution-list-utils';
import type { Moment } from 'moment';
import moment from 'moment';
import {
isEqlRule,
isNewTermsRule,
@ -56,6 +58,7 @@ import { createExceptionItemsReducer } from './reducer';
import { useEditExceptionItems } from './use_edit_exception';
import * as i18n from './translations';
import { ExceptionsExpireTime } from '../flyout_components/expire_time';
interface EditExceptionFlyoutProps {
list: ExceptionListSchema;
@ -119,6 +122,8 @@ const EditExceptionFlyoutComponent: React.FC<EditExceptionFlyoutProps> = ({
disableBulkClose,
bulkCloseIndex,
entryErrorExists,
expireTime,
expireErrorExists,
},
dispatch,
] = useReducer(createExceptionItemsReducer(), {
@ -129,6 +134,8 @@ const EditExceptionFlyoutComponent: React.FC<EditExceptionFlyoutProps> = ({
disableBulkClose: true,
bulkCloseIndex: undefined,
entryErrorExists: false,
expireTime: itemToEdit.expire_time !== undefined ? moment(itemToEdit.expire_time) : undefined,
expireErrorExists: false,
});
const allowLargeValueLists = useMemo((): boolean => {
@ -231,6 +238,26 @@ const EditExceptionFlyoutComponent: React.FC<EditExceptionFlyoutProps> = ({
[dispatch]
);
const setExpireTime = useCallback(
(exceptionExpireTime: Moment | undefined): void => {
dispatch({
type: 'setExpireTime',
expireTime: exceptionExpireTime,
});
},
[dispatch]
);
const setExpireError = useCallback(
(errorExists: boolean): void => {
dispatch({
type: 'setExpireError',
errorExists,
});
},
[dispatch]
);
const handleCloseFlyout = useCallback((): void => {
onCancel(false);
}, [onCancel]);
@ -251,6 +278,7 @@ const EditExceptionFlyoutComponent: React.FC<EditExceptionFlyoutProps> = ({
commentToAdd: newComment,
listType,
selectedOs: itemToEdit.os_types,
expireTime,
items: exceptionItems,
});
@ -292,6 +320,7 @@ const EditExceptionFlyoutComponent: React.FC<EditExceptionFlyoutProps> = ({
onConfirm,
bulkCloseIndex,
onCancel,
expireTime,
]);
const editExceptionMessage = useMemo(
@ -308,8 +337,9 @@ const EditExceptionFlyoutComponent: React.FC<EditExceptionFlyoutProps> = ({
isClosingAlerts ||
exceptionItems.every((item) => item.entries.length === 0) ||
isLoading ||
entryErrorExists,
[isLoading, entryErrorExists, exceptionItems, isSubmitting, isClosingAlerts]
entryErrorExists ||
expireErrorExists,
[isLoading, entryErrorExists, exceptionItems, isSubmitting, isClosingAlerts, expireErrorExists]
);
return (
@ -370,6 +400,12 @@ const EditExceptionFlyoutComponent: React.FC<EditExceptionFlyoutProps> = ({
newCommentValue={newComment}
newCommentOnChange={setComment}
/>
<EuiHorizontalRule />
<ExceptionsExpireTime
expireTime={expireTime}
setExpireTime={setExpireTime}
setExpireError={setExpireError}
/>
{showAlertCloseOptions && (
<>
<EuiHorizontalRule />

View file

@ -6,6 +6,7 @@
*/
import type { ExceptionsBuilderReturnExceptionItem } from '@kbn/securitysolution-list-utils';
import type { Moment } from 'moment';
export interface State {
exceptionItems: ExceptionsBuilderReturnExceptionItem[];
@ -15,6 +16,8 @@ export interface State {
disableBulkClose: boolean;
bulkCloseIndex: string[] | undefined;
entryErrorExists: boolean;
expireTime: Moment | undefined;
expireErrorExists: boolean;
}
export type Action =
@ -45,6 +48,14 @@ export type Action =
| {
type: 'setConditionValidationErrorExists';
errorExists: boolean;
}
| {
type: 'setExpireTime';
expireTime: Moment | undefined;
}
| {
type: 'setExpireError';
errorExists: boolean;
};
export const createExceptionItemsReducer =
@ -110,6 +121,22 @@ export const createExceptionItemsReducer =
entryErrorExists: errorExists,
};
}
case 'setExpireTime': {
const { expireTime } = action;
return {
...state,
expireTime,
};
}
case 'setExpireError': {
const { errorExists } = action;
return {
...state,
expireErrorExists: errorExists,
};
}
default:
return state;
}

View file

@ -51,6 +51,11 @@ export const ExceptionItemCardMetaInfo = memo<ExceptionItemCardMetaInfoProps>(
const onCloseRulesPopover = () => setIsRulesPopoverOpen(false);
const onClosListsPopover = () => setIsListsPopoverOpen(false);
const isExpired = useMemo(
() => (item.expire_time ? new Date(item.expire_time) <= new Date() : false),
[item]
);
const itemActions = useMemo((): EuiContextMenuPanelProps['items'] => {
if (listAndReferences == null) {
return [];
@ -177,6 +182,20 @@ export const ExceptionItemCardMetaInfo = memo<ExceptionItemCardMetaInfoProps>(
dataTestSubj={`${dataTestSubj}-updatedBy`}
/>
</StyledFlexItem>
{item.expire_time != null && (
<>
<StyledFlexItem grow={false}>
<MetaInfoDetails
fieldName="expire_time"
label={
isExpired ? i18n.EXCEPTION_ITEM_EXPIRED_LABEL : i18n.EXCEPTION_ITEM_EXPIRES_LABEL
}
value1={<FormattedDate fieldName="expire_time" value={item.expire_time} />}
dataTestSubj={`${dataTestSubj}-expireTime`}
/>
</StyledFlexItem>
</>
)}
{listAndReferences != null && (
<>
{rulesAffected}
@ -193,7 +212,7 @@ interface MetaInfoDetailsProps {
fieldName: string;
label: string;
value1: JSX.Element | string;
value2: string;
value2?: string;
dataTestSubj: string;
}
@ -210,20 +229,24 @@ const MetaInfoDetails = memo<MetaInfoDetailsProps>(({ label, value1, value2, dat
{value1}
</EuiBadge>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" style={{ fontFamily: 'Inter' }}>
{i18n.EXCEPTION_ITEM_META_BY}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false} data-test-subj={`${dataTestSubj}-value2`}>
<EuiFlexGroup responsive={false} gutterSize="xs" alignItems="center" wrap={false}>
{value2 != null && (
<>
<EuiFlexItem grow={false}>
<EuiBadge color="hollow" style={{ fontFamily: 'Inter' }}>
{value2}
</EuiBadge>
<EuiText size="xs" style={{ fontFamily: 'Inter' }}>
{i18n.EXCEPTION_ITEM_META_BY}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false} data-test-subj={`${dataTestSubj}-value2`}>
<EuiFlexGroup responsive={false} gutterSize="xs" alignItems="center" wrap={false}>
<EuiFlexItem grow={false}>
<EuiBadge color="hollow" style={{ fontFamily: 'Inter' }}>
{value2}
</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</>
)}
</EuiFlexGroup>
);
});

View file

@ -49,6 +49,20 @@ export const EXCEPTION_ITEM_UPDATED_LABEL = i18n.translate(
}
);
export const EXCEPTION_ITEM_EXPIRES_LABEL = i18n.translate(
'xpack.securitySolution.ruleExceptions.exceptionItem.expiresLabel',
{
defaultMessage: 'Expires at',
}
);
export const EXCEPTION_ITEM_EXPIRED_LABEL = i18n.translate(
'xpack.securitySolution.ruleExceptions.exceptionItem.expiredLabel',
{
defaultMessage: 'Expired at',
}
);
export const EXCEPTION_ITEM_META_BY = i18n.translate(
'xpack.securitySolution.ruleExceptions.exceptionItem.metaDetailsBy',
{

View file

@ -0,0 +1,75 @@
/*
* 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 { EuiDatePicker, EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui';
import type { Moment } from 'moment';
import moment from 'moment';
import React, { useCallback, useState } from 'react';
import styled, { css } from 'styled-components';
import * as i18n from './translations';
interface ExceptionItmeExpireTimeProps {
expireTime: Moment | undefined;
setExpireTime: (date: Moment | undefined) => void;
setExpireError: (errorExists: boolean) => void;
}
const SectionHeader = styled(EuiTitle)`
${() => css`
font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
`}
`;
const ExceptionItemExpireTime: React.FC<ExceptionItmeExpireTimeProps> = ({
expireTime,
setExpireTime,
setExpireError,
}): JSX.Element => {
const [dateTime, setDateTime] = useState<Moment | undefined>(expireTime);
const [isInvalid, setIsInvalid] = useState(false);
const [errors, setErrors] = useState<string[]>([]);
const handleChange = useCallback(
(date: Moment | null) => {
setDateTime(date ?? undefined);
setExpireTime(date ?? undefined);
if (date?.isBefore()) {
setIsInvalid(true);
setErrors([i18n.EXCEPTION_EXPIRE_TIME_ERROR]);
setExpireError(true);
} else {
setIsInvalid(false);
setErrors([]);
setExpireError(false);
}
},
[setDateTime, setExpireTime, setExpireError]
);
return (
<div>
<SectionHeader size="xs">
<h3>{i18n.EXCEPTION_EXPIRE_TIME_HEADER}</h3>
</SectionHeader>
<EuiSpacer size="s" />
<EuiFormRow error={errors} isInvalid={isInvalid} label={i18n.EXPIRE_TIME_LABEL}>
<EuiDatePicker
showTimeSelect
selected={dateTime}
isInvalid={isInvalid}
onChange={handleChange}
onClear={() => handleChange(null)}
minDate={moment()}
/>
</EuiFormRow>
</div>
);
};
export const ExceptionsExpireTime = React.memo(ExceptionItemExpireTime);
ExceptionsExpireTime.displayName = 'ExceptionsExpireTime';

View file

@ -0,0 +1,29 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const EXPIRE_TIME_LABEL = i18n.translate(
'xpack.securitySolution.rule_exceptions.flyoutComponents.expireTime.expireTimeLabel',
{
defaultMessage: 'Exception will expire at',
}
);
export const EXCEPTION_EXPIRE_TIME_HEADER = i18n.translate(
'xpack.securitySolution.rule_exceptions.flyoutComponents.expireTime.exceptionExpireTime',
{
defaultMessage: 'Exception Expiration',
}
);
export const EXCEPTION_EXPIRE_TIME_ERROR = i18n.translate(
'xpack.securitySolution.rule_exceptions.flyoutComponents.expireTime.exceptionExpireTimeError',
{
defaultMessage: 'Selected date and time must be in the future.',
}
);

View file

@ -39,6 +39,7 @@ describe('add_exception_flyout#utils', () => {
selectedOs: [],
listType: ExceptionListTypeEnum.RULE_DEFAULT,
items,
expireTime: undefined,
})
).toEqual([
{
@ -75,6 +76,7 @@ describe('add_exception_flyout#utils', () => {
selectedOs: [],
listType: ExceptionListTypeEnum.DETECTION,
items,
expireTime: undefined,
})
).toEqual([
{
@ -114,6 +116,7 @@ describe('add_exception_flyout#utils', () => {
selectedOs: ['windows'],
listType: ExceptionListTypeEnum.ENDPOINT,
items,
expireTime: undefined,
})
).toEqual([
{

View file

@ -13,6 +13,7 @@ import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import type { ExceptionsBuilderReturnExceptionItem } from '@kbn/securitysolution-list-utils';
import type { HorizontalAlignment } from '@elastic/eui';
import type { Moment } from 'moment';
import {
HeaderMenu,
generateLinkedRulesMenuItems,
@ -22,6 +23,7 @@ import { ListDetailsLinkAnchor } from '../../../../exceptions/components';
import {
enrichExceptionItemsWithOS,
enrichNewExceptionItemsWithComments,
enrichNewExceptionItemsWithExpireTime,
enrichNewExceptionItemsWithName,
enrichRuleExceptions,
enrichSharedExceptions,
@ -58,6 +60,18 @@ export const enrichItemWithName =
return itemName.trim() !== '' ? enrichNewExceptionItemsWithName(items, itemName) : items;
};
/**
* Adds expiration datetime to all new exceptionItems
* @param expireTimeToAdd new expireTime to add to item
*/
export const enrichItemWithExpireTime =
(expireTimeToAdd: Moment | undefined) =>
(items: ExceptionsBuilderReturnExceptionItem[]): ExceptionsBuilderReturnExceptionItem[] => {
return expireTimeToAdd != null
? enrichNewExceptionItemsWithExpireTime(items, expireTimeToAdd)
: items;
};
/**
* Modifies item entries to be in correct format and adds os selection to items
* @param listType exception list type
@ -114,6 +128,7 @@ export const enrichItemsForSharedLists =
* @param sharedLists shared exception lists that were selected to add items to
* @param selectedOs os selection
* @param listType exception list type
* @param expireTime exception item expire time
* @param items exception items to be modified
*/
export const enrichNewExceptionItems = ({
@ -124,6 +139,7 @@ export const enrichNewExceptionItems = ({
sharedLists,
selectedOs,
listType,
expireTime,
items,
}: {
itemName: string;
@ -133,10 +149,12 @@ export const enrichNewExceptionItems = ({
addToSharedLists: boolean;
sharedLists: ExceptionListSchema[];
listType: ExceptionListTypeEnum;
expireTime: Moment | undefined;
items: ExceptionsBuilderReturnExceptionItem[];
}): ExceptionsBuilderReturnExceptionItem[] => {
const enriched: ExceptionsBuilderReturnExceptionItem[] = pipe(
enrichItemWithComment(commentToAdd),
enrichItemWithExpireTime(expireTime),
enrichItemWithName(itemName),
enrichEndpointItems(listType, selectedOs),
enrichItemsForDefaultRuleList(listType, addToRules),
@ -155,6 +173,7 @@ export const enrichNewExceptionItems = ({
* @param sharedLists shared exception lists that were selected to add items to
* @param selectedOs os selection
* @param listType exception list type
* @param expireTime exception item expire time
* @param items exception items to be modified
*/
export const enrichExceptionItemsForUpdate = ({
@ -162,16 +181,19 @@ export const enrichExceptionItemsForUpdate = ({
commentToAdd,
selectedOs,
listType,
expireTime,
items,
}: {
itemName: string;
commentToAdd: string;
selectedOs: OsType[];
listType: ExceptionListTypeEnum;
expireTime: Moment | undefined;
items: ExceptionsBuilderReturnExceptionItem[];
}): ExceptionsBuilderReturnExceptionItem[] => {
const enriched: ExceptionsBuilderReturnExceptionItem[] = pipe(
enrichItemWithComment(commentToAdd),
enrichItemWithExpireTime(expireTime),
enrichItemWithName(itemName),
enrichEndpointItems(listType, selectedOs)
)(items);

View file

@ -9,6 +9,7 @@ import React from 'react';
import type { EuiCommentProps } from '@elastic/eui';
import { EuiText, EuiAvatar } from '@elastic/eui';
import { capitalize, omit } from 'lodash';
import type { Moment } from 'moment';
import moment from 'moment';
import type {
@ -177,6 +178,23 @@ export const enrichNewExceptionItemsWithComments = (
});
};
/**
* Adds expireTime to all new exceptionItems if not present already
* @param exceptionItems new or existing ExceptionItem[]
* @param expireTime new expireTime
*/
export const enrichNewExceptionItemsWithExpireTime = (
exceptionItems: ExceptionsBuilderReturnExceptionItem[],
expireTime: Moment
): ExceptionsBuilderReturnExceptionItem[] => {
return exceptionItems.map((item: ExceptionsBuilderReturnExceptionItem) => {
return {
...item,
expire_time: expireTime.toISOString(),
};
});
};
export const buildGetAlertByIdQuery = (id: string | undefined) => ({
query: {
match: {

View file

@ -78,6 +78,7 @@ export const getExceptionListItemSchemaMock = (
created_by: USER,
description: DESCRIPTION,
entries: ENTRIES,
expire_time: undefined,
id: '1',
item_id: 'endpoint_list_item',
list_id: 'endpoint_list_id',

View file

@ -33,6 +33,7 @@ import { ListExceptionItems } from '../list_exception_items';
import { useListDetailsView } from '../../hooks';
import { useExceptionsListCard } from '../../hooks/use_exceptions_list.card';
import { ManageRules } from '../manage_rules';
import { ExportExceptionsListModal } from '../export_exceptions_list_modal';
interface ExceptionsListCardProps {
exceptionsList: ExceptionListInfo;
@ -47,10 +48,12 @@ interface ExceptionsListCardProps {
}) => () => Promise<void>;
handleExport: ({
id,
includeExpiredExceptions,
listId,
namespaceType,
}: {
id: string;
includeExpiredExceptions: boolean;
listId: string;
namespaceType: NamespaceType;
}) => () => Promise<void>;
@ -115,6 +118,9 @@ export const ExceptionsListCard = memo<ExceptionsListCardProps>(
emptyViewerTitle,
emptyViewerBody,
emptyViewerButtonText,
handleCancelExportModal,
handleConfirmExportModal,
showExportModal,
} = useExceptionsListCard({
exceptionsList,
handleExport,
@ -248,6 +254,12 @@ export const ExceptionsListCard = memo<ExceptionsListCardProps>(
onRuleSelectionChange={onRuleSelectionChange}
/>
) : null}
{showExportModal ? (
<ExportExceptionsListModal
handleCloseModal={handleCancelExportModal}
onModalConfirm={handleConfirmExportModal}
/>
) : null}
</EuiFlexGroup>
);
}

View file

@ -0,0 +1,50 @@
/*
* 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 React, { memo, useCallback, useState } from 'react';
import { EuiConfirmModal, EuiSwitch } from '@elastic/eui';
import * as i18n from '../../translations';
interface ExportExceptionsListModalProps {
handleCloseModal: () => void;
onModalConfirm: (includeExpired: boolean) => void;
}
export const ExportExceptionsListModal = memo<ExportExceptionsListModalProps>(
({ handleCloseModal, onModalConfirm }) => {
const [exportExpired, setExportExpired] = useState(true);
const handleSwitchChange = useCallback(() => {
setExportExpired(!exportExpired);
}, [setExportExpired, exportExpired]);
const handleConfirm = useCallback(() => {
onModalConfirm(exportExpired);
handleCloseModal();
}, [exportExpired, handleCloseModal, onModalConfirm]);
return (
<EuiConfirmModal
title={i18n.EXPORT_MODAL_TITLE}
onCancel={handleCloseModal}
onConfirm={handleConfirm}
cancelButtonText={i18n.EXPORT_MODAL_CANCEL_BUTTON}
confirmButtonText={i18n.EXPORT_MODAL_CONFIRM_BUTTON}
defaultFocusedButton="confirm"
>
<EuiSwitch
label={i18n.EXPORT_MODAL_INCLUDE_SWITCH_LABEL}
checked={exportExpired}
onChange={handleSwitchChange}
/>
</EuiConfirmModal>
);
}
);
ExportExceptionsListModal.displayName = 'ExportExceptionsListModal';

View file

@ -21,6 +21,12 @@ import { useListExceptionItems } from '../use_list_exception_items';
import * as i18n from '../../translations';
import { checkIfListCannotBeEdited } from '../../utils/list.utils';
interface ExportListAction {
id: string;
listId: string;
namespaceType: NamespaceType;
includeExpiredExceptions: boolean;
}
interface ListAction {
id: string;
listId: string;
@ -33,7 +39,12 @@ export const useExceptionsListCard = ({
handleManageRules,
}: {
exceptionsList: ExceptionListInfo;
handleExport: ({ id, listId, namespaceType }: ListAction) => () => Promise<void>;
handleExport: ({
id,
listId,
namespaceType,
includeExpiredExceptions,
}: ExportListAction) => () => Promise<void>;
handleDelete: ({ id, listId, namespaceType }: ListAction) => () => Promise<void>;
handleManageRules: () => void;
}) => {
@ -41,6 +52,7 @@ export const useExceptionsListCard = ({
const [exceptionToEdit, setExceptionToEdit] = useState<ExceptionListItemSchema>();
const [showAddExceptionFlyout, setShowAddExceptionFlyout] = useState(false);
const [showEditExceptionFlyout, setShowEditExceptionFlyout] = useState(false);
const [showExportModal, setShowExportModal] = useState(false);
const {
name: listName,
@ -111,13 +123,7 @@ export const useExceptionsListCard = ({
key: 'Export',
icon: 'exportAction',
label: i18n.EXPORT_EXCEPTION_LIST,
onClick: (e: React.MouseEvent<Element, MouseEvent>) => {
handleExport({
id: exceptionsList.id,
listId: exceptionsList.list_id,
namespaceType: exceptionsList.namespace_type,
})();
},
onClick: (e: React.MouseEvent<Element, MouseEvent>) => setShowExportModal(true),
},
{
key: 'Delete',
@ -147,7 +153,7 @@ export const useExceptionsListCard = ({
exceptionsList.list_id,
exceptionsList.namespace_type,
handleDelete,
handleExport,
setShowExportModal,
listCannotBeEdited,
handleManageRules,
]
@ -173,6 +179,26 @@ export const useExceptionsListCard = ({
[fetchItems, setShowAddExceptionFlyout, setShowEditExceptionFlyout]
);
const onExportListClick = useCallback(() => {
setShowExportModal(true);
}, [setShowExportModal]);
const handleCancelExportModal = () => {
setShowExportModal(false);
};
const handleConfirmExportModal = useCallback(
(includeExpiredExceptions: boolean): void => {
handleExport({
id: exceptionsList.id,
listId: exceptionsList.list_id,
namespaceType: exceptionsList.namespace_type,
includeExpiredExceptions,
})();
},
[handleExport, exceptionsList]
);
// routes to x-pack/plugins/security_solution/public/exceptions/routes.tsx
// details component is here: x-pack/plugins/security_solution/public/exceptions/pages/list_detail_view/index.tsx
const { onClick: goToExceptionDetail } = useGetSecuritySolutionLinkProps()({
@ -211,5 +237,9 @@ export const useExceptionsListCard = ({
emptyViewerTitle,
emptyViewerBody,
emptyViewerButtonText,
showExportModal,
onExportListClick,
handleCancelExportModal,
handleConfirmExportModal,
};
};

View file

@ -163,28 +163,32 @@ export const useListDetailsView = (exceptionListId: string) => {
},
[exceptionListId, handleErrorStatus, http, list]
);
const onExportList = useCallback(async () => {
try {
if (!list) return;
await exportExceptionList({
id: list.id,
listId: list.list_id,
namespaceType: list.namespace_type,
onError: (error: Error) => handleErrorStatus(error),
onSuccess: (blob) => {
setExportedList(blob);
toasts?.addSuccess(i18n.EXCEPTION_LIST_EXPORTED_SUCCESSFULLY(list.list_id));
},
});
} catch (error) {
handleErrorStatus(
error,
undefined,
i18n.EXCEPTION_EXPORT_ERROR,
i18n.EXCEPTION_EXPORT_ERROR_DESCRIPTION
);
}
}, [list, exportExceptionList, handleErrorStatus, toasts]);
const onExportList = useCallback(
async (includeExpiredExceptions: boolean) => {
try {
if (!list) return;
await exportExceptionList({
id: list.id,
listId: list.list_id,
includeExpiredExceptions,
namespaceType: list.namespace_type,
onError: (error: Error) => handleErrorStatus(error),
onSuccess: (blob) => {
setExportedList(blob);
toasts?.addSuccess(i18n.EXCEPTION_LIST_EXPORTED_SUCCESSFULLY(list.list_id));
},
});
} catch (error) {
handleErrorStatus(
error,
undefined,
i18n.EXCEPTION_EXPORT_ERROR,
i18n.EXCEPTION_EXPORT_ERROR_DESCRIPTION
);
}
},
[list, exportExceptionList, handleErrorStatus, toasts]
);
// #region DeleteList

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import type { FC } from 'react';
import {
@ -24,6 +24,7 @@ import { AutoDownload } from '../../../common/components/auto_download/auto_down
import { ListWithSearch, ManageRules, ListDetailsLinkAnchor } from '../../components';
import { useListDetailsView } from '../../hooks';
import * as i18n from '../../translations';
import { ExportExceptionsListModal } from '../../components/export_exceptions_list_modal';
export const ListsDetailViewComponent: FC = () => {
const { detailName: exceptionListId } = useParams<{
@ -59,6 +60,12 @@ export const ListsDetailViewComponent: FC = () => {
handleReferenceDelete,
} = useListDetailsView(exceptionListId);
const [showExportModal, setShowExportModal] = useState(false);
const onModalClose = useCallback(() => setShowExportModal(false), [setShowExportModal]);
const onModalOpen = useCallback(() => setShowExportModal(true), [setShowExportModal]);
const detailsViewContent = useMemo(() => {
if (viewerStatus === ViewerStatus.ERROR)
return <EmptyViewerState isReadOnly={isReadOnly} viewerStatus={viewerStatus} />;
@ -79,7 +86,7 @@ export const ListsDetailViewComponent: FC = () => {
backOptions={headerBackOptions}
securityLinkAnchorComponent={ListDetailsLinkAnchor}
onEditListDetails={onEditListDetails}
onExportList={onExportList}
onExportList={onModalOpen}
onDeleteList={handleDelete}
onManageRules={onManageRules}
/>
@ -107,6 +114,12 @@ export const ListsDetailViewComponent: FC = () => {
onRuleSelectionChange={onRuleSelectionChange}
/>
) : null}
{showExportModal && (
<ExportExceptionsListModal
onModalConfirm={onExportList}
handleCloseModal={onModalClose}
/>
)}
</>
);
}, [
@ -128,6 +141,7 @@ export const ListsDetailViewComponent: FC = () => {
showManageButtonLoader,
showManageRulesFlyout,
showReferenceErrorModal,
showExportModal,
viewerStatus,
onCancelManageRules,
onEditListDetails,
@ -138,6 +152,8 @@ export const ListsDetailViewComponent: FC = () => {
handleCloseReferenceErrorModal,
handleDelete,
handleReferenceDelete,
onModalClose,
onModalOpen,
]);
return (
<>

View file

@ -192,10 +192,21 @@ export const SharedLists = React.memo(() => {
);
const handleExport = useCallback(
({ id, listId, namespaceType }: { id: string; listId: string; namespaceType: NamespaceType }) =>
({
id,
listId,
namespaceType,
includeExpiredExceptions,
}: {
id: string;
listId: string;
namespaceType: NamespaceType;
includeExpiredExceptions: boolean;
}) =>
async () => {
await exportExceptionList({
id,
includeExpiredExceptions,
listId,
namespaceType,
onError: handleExportError,

View file

@ -358,3 +358,31 @@ export const SORT_BY_CREATE_AT = i18n.translate(
defaultMessage: 'Created At',
}
);
export const EXPORT_MODAL_CANCEL_BUTTON = i18n.translate(
'xpack.securitySolution.exceptions.exportModalCancelButton',
{
defaultMessage: 'Cancel',
}
);
export const EXPORT_MODAL_TITLE = i18n.translate(
'xpack.securitySolution.exceptions.exportModalTitle',
{
defaultMessage: 'Export exception list',
}
);
export const EXPORT_MODAL_INCLUDE_SWITCH_LABEL = i18n.translate(
'xpack.securitySolution.exceptions.exportModalIncludeSwitchLabel',
{
defaultMessage: 'Include expired exceptions',
}
);
export const EXPORT_MODAL_CONFIRM_BUTTON = i18n.translate(
'xpack.securitySolution.exceptions.exportModalConfirmButton',
{
defaultMessage: 'Export',
}
);

View file

@ -97,6 +97,7 @@ export const createNonInteractiveSessionEventFilter = (
itemId: uuidv4(),
meta: [],
comments: [],
expireTime: undefined,
});
} catch (err) {
logger.error(`Error creating Event Filter: ${wrapErrorIfNeeded(err)}`);

View file

@ -57,6 +57,7 @@ export const removePolicyFromArtifacts = async (
namespaceType: artifact.namespace_type,
osTypes: artifact.os_types,
tags: artifact.tags.filter((currentPolicy) => currentPolicy !== `policy:${policy.id}`),
expireTime: artifact.expire_time,
}),
{
/** Number of concurrent executions till the end of the artifacts array */

View file

@ -190,6 +190,7 @@ export const createExceptionListItems = async ({
comments: item.comments,
description: item.description,
entries: item.entries,
expireTime: item.expire_time,
itemId: item.item_id,
listId: defaultList.list_id,
meta: item.meta,

View file

@ -94,6 +94,7 @@ export const createPromises = (
id,
listId,
namespaceType,
includeExpiredExceptions: true, // TODO: pass this arg in via the rule export api
});
}
);

View file

@ -313,6 +313,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
});
const { filter: exceptionFilter, unprocessedExceptions } = await buildExceptionFilter({
startedAt,
alias: null,
excludeExceptions: true,
chunkSize: 10,

View file

@ -789,6 +789,7 @@ describe('create_signals', () => {
alias: null,
chunkSize: 1024,
excludeExceptions: true,
startedAt: new Date(),
});
const request = buildEqlSearchRequest({
query: 'process where true',

Some files were not shown because too many files have changed in this diff Show more